Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/scrollbar/scrollableElement.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { getZoomFactor, isChrome } from '../../browser.js';
7
import * as dom from '../../dom.js';
8
import { FastDomNode, createFastDomNode } from '../../fastDomNode.js';
9
import { IMouseEvent, IMouseWheelEvent, StandardWheelEvent } from '../../mouseEvent.js';
10
import { ScrollbarHost } from './abstractScrollbar.js';
11
import { HorizontalScrollbar } from './horizontalScrollbar.js';
12
import { ScrollableElementChangeOptions, ScrollableElementCreationOptions, ScrollableElementResolvedOptions } from './scrollableElementOptions.js';
13
import { VerticalScrollbar } from './verticalScrollbar.js';
14
import { Widget } from '../widget.js';
15
import { TimeoutTimer } from '../../../common/async.js';
16
import { Emitter, Event } from '../../../common/event.js';
17
import { IDisposable, dispose } from '../../../common/lifecycle.js';
18
import * as platform from '../../../common/platform.js';
19
import { INewScrollDimensions, INewScrollPosition, IScrollDimensions, IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from '../../../common/scrollable.js';
20
import './media/scrollbars.css';
21
22
const HIDE_TIMEOUT = 500;
23
const SCROLL_WHEEL_SENSITIVITY = 50;
24
const SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED = true;
25
26
export interface IOverviewRulerLayoutInfo {
27
parent: HTMLElement;
28
insertBefore: HTMLElement;
29
}
30
31
class MouseWheelClassifierItem {
32
public timestamp: number;
33
public deltaX: number;
34
public deltaY: number;
35
public score: number;
36
37
constructor(timestamp: number, deltaX: number, deltaY: number) {
38
this.timestamp = timestamp;
39
this.deltaX = deltaX;
40
this.deltaY = deltaY;
41
this.score = 0;
42
}
43
}
44
45
export class MouseWheelClassifier {
46
47
public static readonly INSTANCE = new MouseWheelClassifier();
48
49
private readonly _capacity: number;
50
private _memory: MouseWheelClassifierItem[];
51
private _front: number;
52
private _rear: number;
53
54
constructor() {
55
this._capacity = 5;
56
this._memory = [];
57
this._front = -1;
58
this._rear = -1;
59
}
60
61
public isPhysicalMouseWheel(): boolean {
62
if (this._front === -1 && this._rear === -1) {
63
// no elements
64
return false;
65
}
66
67
// 0.5 * last + 0.25 * 2nd last + 0.125 * 3rd last + ...
68
let remainingInfluence = 1;
69
let score = 0;
70
let iteration = 1;
71
72
let index = this._rear;
73
do {
74
const influence = (index === this._front ? remainingInfluence : Math.pow(2, -iteration));
75
remainingInfluence -= influence;
76
score += this._memory[index].score * influence;
77
78
if (index === this._front) {
79
break;
80
}
81
82
index = (this._capacity + index - 1) % this._capacity;
83
iteration++;
84
} while (true);
85
86
return (score <= 0.5);
87
}
88
89
public acceptStandardWheelEvent(e: StandardWheelEvent): void {
90
if (isChrome) {
91
const targetWindow = dom.getWindow(e.browserEvent);
92
const pageZoomFactor = getZoomFactor(targetWindow);
93
// On Chrome, the incoming delta events are multiplied with the OS zoom factor.
94
// The OS zoom factor can be reverse engineered by using the device pixel ratio and the configured zoom factor into account.
95
this.accept(Date.now(), e.deltaX * pageZoomFactor, e.deltaY * pageZoomFactor);
96
} else {
97
this.accept(Date.now(), e.deltaX, e.deltaY);
98
}
99
}
100
101
public accept(timestamp: number, deltaX: number, deltaY: number): void {
102
let previousItem = null;
103
const item = new MouseWheelClassifierItem(timestamp, deltaX, deltaY);
104
105
if (this._front === -1 && this._rear === -1) {
106
this._memory[0] = item;
107
this._front = 0;
108
this._rear = 0;
109
} else {
110
previousItem = this._memory[this._rear];
111
112
this._rear = (this._rear + 1) % this._capacity;
113
if (this._rear === this._front) {
114
// Drop oldest
115
this._front = (this._front + 1) % this._capacity;
116
}
117
this._memory[this._rear] = item;
118
}
119
120
item.score = this._computeScore(item, previousItem);
121
}
122
123
/**
124
* A score between 0 and 1 for `item`.
125
* - a score towards 0 indicates that the source appears to be a physical mouse wheel
126
* - a score towards 1 indicates that the source appears to be a touchpad or magic mouse, etc.
127
*/
128
private _computeScore(item: MouseWheelClassifierItem, previousItem: MouseWheelClassifierItem | null): number {
129
130
if (Math.abs(item.deltaX) > 0 && Math.abs(item.deltaY) > 0) {
131
// both axes exercised => definitely not a physical mouse wheel
132
return 1;
133
}
134
135
let score: number = 0.5;
136
137
if (!this._isAlmostInt(item.deltaX) || !this._isAlmostInt(item.deltaY)) {
138
// non-integer deltas => indicator that this is not a physical mouse wheel
139
score += 0.25;
140
}
141
142
// Non-accelerating scroll => indicator that this is a physical mouse wheel
143
// These can be identified by seeing whether they are the module of one another.
144
if (previousItem) {
145
const absDeltaX = Math.abs(item.deltaX);
146
const absDeltaY = Math.abs(item.deltaY);
147
148
const absPreviousDeltaX = Math.abs(previousItem.deltaX);
149
const absPreviousDeltaY = Math.abs(previousItem.deltaY);
150
151
// Min 1 to avoid division by zero, module 1 will still be 0.
152
const minDeltaX = Math.max(Math.min(absDeltaX, absPreviousDeltaX), 1);
153
const minDeltaY = Math.max(Math.min(absDeltaY, absPreviousDeltaY), 1);
154
155
const maxDeltaX = Math.max(absDeltaX, absPreviousDeltaX);
156
const maxDeltaY = Math.max(absDeltaY, absPreviousDeltaY);
157
158
const isSameModulo = (maxDeltaX % minDeltaX === 0 && maxDeltaY % minDeltaY === 0);
159
if (isSameModulo) {
160
score -= 0.5;
161
}
162
}
163
164
return Math.min(Math.max(score, 0), 1);
165
}
166
167
private _isAlmostInt(value: number): boolean {
168
const epsilon = Number.EPSILON * 100; // Use a small tolerance factor for floating-point errors
169
const delta = Math.abs(Math.round(value) - value);
170
return (delta < 0.01 + epsilon);
171
}
172
}
173
174
export abstract class AbstractScrollableElement extends Widget {
175
176
private readonly _options: ScrollableElementResolvedOptions;
177
protected readonly _scrollable: Scrollable;
178
private readonly _verticalScrollbar: VerticalScrollbar;
179
private readonly _horizontalScrollbar: HorizontalScrollbar;
180
private readonly _domNode: HTMLElement;
181
182
private readonly _leftShadowDomNode: FastDomNode<HTMLElement> | null;
183
private readonly _topShadowDomNode: FastDomNode<HTMLElement> | null;
184
private readonly _topLeftShadowDomNode: FastDomNode<HTMLElement> | null;
185
186
private readonly _listenOnDomNode: HTMLElement;
187
188
private _mouseWheelToDispose: IDisposable[];
189
190
private _isDragging: boolean;
191
private _mouseIsOver: boolean;
192
193
private readonly _hideTimeout: TimeoutTimer;
194
private _shouldRender: boolean;
195
196
private _revealOnScroll: boolean;
197
198
private _inertialTimeout: TimeoutTimer | null = null;
199
private _inertialSpeed: { X: number; Y: number } = { X: 0, Y: 0 };
200
201
private readonly _onScroll = this._register(new Emitter<ScrollEvent>());
202
public get onScroll(): Event<ScrollEvent> { return this._onScroll.event; }
203
204
private readonly _onWillScroll = this._register(new Emitter<ScrollEvent>());
205
public get onWillScroll(): Event<ScrollEvent> { return this._onWillScroll.event; }
206
207
public get options(): Readonly<ScrollableElementResolvedOptions> {
208
return this._options;
209
}
210
211
protected constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
212
super();
213
element.style.overflow = 'hidden';
214
this._options = resolveOptions(options);
215
this._scrollable = scrollable;
216
217
this._register(this._scrollable.onScroll((e) => {
218
this._onWillScroll.fire(e);
219
this._onDidScroll(e);
220
this._onScroll.fire(e);
221
}));
222
223
const scrollbarHost: ScrollbarHost = {
224
onMouseWheel: (mouseWheelEvent: StandardWheelEvent) => this._onMouseWheel(mouseWheelEvent),
225
onDragStart: () => this._onDragStart(),
226
onDragEnd: () => this._onDragEnd(),
227
};
228
this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this._options, scrollbarHost));
229
this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this._options, scrollbarHost));
230
231
this._domNode = document.createElement('div');
232
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
233
this._domNode.setAttribute('role', 'presentation');
234
this._domNode.style.position = 'relative';
235
this._domNode.style.overflow = 'hidden';
236
this._domNode.appendChild(element);
237
this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode);
238
this._domNode.appendChild(this._verticalScrollbar.domNode.domNode);
239
240
if (this._options.useShadows) {
241
this._leftShadowDomNode = createFastDomNode(document.createElement('div'));
242
this._leftShadowDomNode.setClassName('shadow');
243
this._domNode.appendChild(this._leftShadowDomNode.domNode);
244
245
this._topShadowDomNode = createFastDomNode(document.createElement('div'));
246
this._topShadowDomNode.setClassName('shadow');
247
this._domNode.appendChild(this._topShadowDomNode.domNode);
248
249
this._topLeftShadowDomNode = createFastDomNode(document.createElement('div'));
250
this._topLeftShadowDomNode.setClassName('shadow');
251
this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
252
} else {
253
this._leftShadowDomNode = null;
254
this._topShadowDomNode = null;
255
this._topLeftShadowDomNode = null;
256
}
257
258
this._listenOnDomNode = this._options.listenOnDomNode || this._domNode;
259
260
this._mouseWheelToDispose = [];
261
this._setListeningToMouseWheel(this._options.handleMouseWheel);
262
263
this.onmouseover(this._listenOnDomNode, (e) => this._onMouseOver(e));
264
this.onmouseleave(this._listenOnDomNode, (e) => this._onMouseLeave(e));
265
266
this._hideTimeout = this._register(new TimeoutTimer());
267
this._isDragging = false;
268
this._mouseIsOver = false;
269
270
this._shouldRender = true;
271
272
this._revealOnScroll = true;
273
}
274
275
public override dispose(): void {
276
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
277
if (this._inertialTimeout) {
278
this._inertialTimeout.dispose();
279
this._inertialTimeout = null;
280
}
281
super.dispose();
282
}
283
284
/**
285
* Get the generated 'scrollable' dom node
286
*/
287
public getDomNode(): HTMLElement {
288
return this._domNode;
289
}
290
291
public getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo {
292
return {
293
parent: this._domNode,
294
insertBefore: this._verticalScrollbar.domNode.domNode,
295
};
296
}
297
298
/**
299
* Delegate a pointer down event to the vertical scrollbar.
300
* This is to help with clicking somewhere else and having the scrollbar react.
301
*/
302
public delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent): void {
303
this._verticalScrollbar.delegatePointerDown(browserEvent);
304
}
305
306
public getScrollDimensions(): IScrollDimensions {
307
return this._scrollable.getScrollDimensions();
308
}
309
310
public setScrollDimensions(dimensions: INewScrollDimensions): void {
311
this._scrollable.setScrollDimensions(dimensions, false);
312
}
313
314
/**
315
* Update the class name of the scrollable element.
316
*/
317
public updateClassName(newClassName: string): void {
318
this._options.className = newClassName;
319
// Defaults are different on Macs
320
if (platform.isMacintosh) {
321
this._options.className += ' mac';
322
}
323
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
324
}
325
326
/**
327
* Update configuration options for the scrollbar.
328
*/
329
public updateOptions(newOptions: ScrollableElementChangeOptions): void {
330
if (typeof newOptions.handleMouseWheel !== 'undefined') {
331
this._options.handleMouseWheel = newOptions.handleMouseWheel;
332
this._setListeningToMouseWheel(this._options.handleMouseWheel);
333
}
334
if (typeof newOptions.mouseWheelScrollSensitivity !== 'undefined') {
335
this._options.mouseWheelScrollSensitivity = newOptions.mouseWheelScrollSensitivity;
336
}
337
if (typeof newOptions.fastScrollSensitivity !== 'undefined') {
338
this._options.fastScrollSensitivity = newOptions.fastScrollSensitivity;
339
}
340
if (typeof newOptions.scrollPredominantAxis !== 'undefined') {
341
this._options.scrollPredominantAxis = newOptions.scrollPredominantAxis;
342
}
343
if (typeof newOptions.horizontal !== 'undefined') {
344
this._options.horizontal = newOptions.horizontal;
345
}
346
if (typeof newOptions.vertical !== 'undefined') {
347
this._options.vertical = newOptions.vertical;
348
}
349
if (typeof newOptions.horizontalScrollbarSize !== 'undefined') {
350
this._options.horizontalScrollbarSize = newOptions.horizontalScrollbarSize;
351
}
352
if (typeof newOptions.verticalScrollbarSize !== 'undefined') {
353
this._options.verticalScrollbarSize = newOptions.verticalScrollbarSize;
354
}
355
if (typeof newOptions.scrollByPage !== 'undefined') {
356
this._options.scrollByPage = newOptions.scrollByPage;
357
}
358
this._horizontalScrollbar.updateOptions(this._options);
359
this._verticalScrollbar.updateOptions(this._options);
360
361
if (!this._options.lazyRender) {
362
this._render();
363
}
364
}
365
366
public setRevealOnScroll(value: boolean) {
367
this._revealOnScroll = value;
368
}
369
370
public delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
371
this._onMouseWheel(new StandardWheelEvent(browserEvent));
372
}
373
374
private async _periodicSync(): Promise<void> {
375
let scheduleAgain = false;
376
377
if (this._inertialSpeed.X !== 0 || this._inertialSpeed.Y !== 0) {
378
this._scrollable.setScrollPositionNow({
379
scrollTop: this._scrollable.getCurrentScrollPosition().scrollTop - this._inertialSpeed.Y * 100,
380
scrollLeft: this._scrollable.getCurrentScrollPosition().scrollLeft - this._inertialSpeed.X * 100
381
});
382
this._inertialSpeed.X *= 0.9;
383
this._inertialSpeed.Y *= 0.9;
384
if (Math.abs(this._inertialSpeed.X) < 0.01) {
385
this._inertialSpeed.X = 0;
386
}
387
if (Math.abs(this._inertialSpeed.Y) < 0.01) {
388
this._inertialSpeed.Y = 0;
389
}
390
391
scheduleAgain = (this._inertialSpeed.X !== 0 || this._inertialSpeed.Y !== 0);
392
}
393
394
if (scheduleAgain) {
395
if (!this._inertialTimeout) {
396
this._inertialTimeout = new TimeoutTimer();
397
}
398
this._inertialTimeout.cancelAndSet(() => this._periodicSync(), 1000 / 60);
399
} else {
400
this._inertialTimeout?.dispose();
401
this._inertialTimeout = null;
402
}
403
}
404
405
// -------------------- mouse wheel scrolling --------------------
406
407
private _setListeningToMouseWheel(shouldListen: boolean): void {
408
const isListening = (this._mouseWheelToDispose.length > 0);
409
410
if (isListening === shouldListen) {
411
// No change
412
return;
413
}
414
415
// Stop listening (if necessary)
416
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
417
418
// Start listening (if necessary)
419
if (shouldListen) {
420
const onMouseWheel = (browserEvent: IMouseWheelEvent) => {
421
this._onMouseWheel(new StandardWheelEvent(browserEvent));
422
};
423
424
this._mouseWheelToDispose.push(dom.addDisposableListener(this._listenOnDomNode, dom.EventType.MOUSE_WHEEL, onMouseWheel, { passive: false }));
425
}
426
}
427
428
private _onMouseWheel(e: StandardWheelEvent): void {
429
if (e.browserEvent?.defaultPrevented) {
430
return;
431
}
432
433
const classifier = MouseWheelClassifier.INSTANCE;
434
if (SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED) {
435
classifier.acceptStandardWheelEvent(e);
436
}
437
438
// useful for creating unit tests:
439
// console.log(`${Date.now()}, ${e.deltaY}, ${e.deltaX}`);
440
441
let didScroll = false;
442
443
if (e.deltaY || e.deltaX) {
444
let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
445
let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
446
447
if (this._options.scrollPredominantAxis) {
448
if (this._options.scrollYToX && deltaX + deltaY === 0) {
449
// when configured to map Y to X and we both see
450
// no dominant axis and X and Y are competing with
451
// identical values into opposite directions, we
452
// ignore the delta as we cannot make a decision then
453
deltaX = deltaY = 0;
454
} else if (Math.abs(deltaY) >= Math.abs(deltaX)) {
455
deltaX = 0;
456
} else {
457
deltaY = 0;
458
}
459
}
460
461
if (this._options.flipAxes) {
462
[deltaY, deltaX] = [deltaX, deltaY];
463
}
464
465
// Convert vertical scrolling to horizontal if shift is held, this
466
// is handled at a higher level on Mac
467
const shiftConvert = !platform.isMacintosh && e.browserEvent && e.browserEvent.shiftKey;
468
if ((this._options.scrollYToX || shiftConvert) && !deltaX) {
469
deltaX = deltaY;
470
deltaY = 0;
471
}
472
473
if (e.browserEvent && e.browserEvent.altKey) {
474
// fastScrolling
475
deltaX = deltaX * this._options.fastScrollSensitivity;
476
deltaY = deltaY * this._options.fastScrollSensitivity;
477
}
478
479
const futureScrollPosition = this._scrollable.getFutureScrollPosition();
480
481
let desiredScrollPosition: INewScrollPosition = {};
482
if (deltaY) {
483
const deltaScrollTop = SCROLL_WHEEL_SENSITIVITY * deltaY;
484
// Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
485
const desiredScrollTop = futureScrollPosition.scrollTop - (deltaScrollTop < 0 ? Math.floor(deltaScrollTop) : Math.ceil(deltaScrollTop));
486
this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
487
}
488
if (deltaX) {
489
const deltaScrollLeft = SCROLL_WHEEL_SENSITIVITY * deltaX;
490
// Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
491
const desiredScrollLeft = futureScrollPosition.scrollLeft - (deltaScrollLeft < 0 ? Math.floor(deltaScrollLeft) : Math.ceil(deltaScrollLeft));
492
this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
493
}
494
495
// Check that we are scrolling towards a location which is valid
496
desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition);
497
498
if (this._options.inertialScroll && (deltaX || deltaY)) {
499
let startPeriodic = false;
500
// Only start periodic if it's not running
501
if (this._inertialSpeed.X === 0 && this._inertialSpeed.Y === 0) {
502
startPeriodic = true;
503
}
504
this._inertialSpeed.Y = (deltaY < 0 ? -1 : 1) * (Math.abs(deltaY) ** 1.02);
505
this._inertialSpeed.X = (deltaX < 0 ? -1 : 1) * (Math.abs(deltaX) ** 1.02);
506
if (startPeriodic) {
507
this._periodicSync();
508
}
509
}
510
511
if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) {
512
513
const canPerformSmoothScroll = (
514
SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED
515
&& this._options.mouseWheelSmoothScroll
516
&& classifier.isPhysicalMouseWheel()
517
);
518
519
if (canPerformSmoothScroll) {
520
this._scrollable.setScrollPositionSmooth(desiredScrollPosition);
521
} else {
522
this._scrollable.setScrollPositionNow(desiredScrollPosition);
523
}
524
525
didScroll = true;
526
}
527
}
528
529
let consumeMouseWheel = didScroll;
530
if (!consumeMouseWheel && this._options.alwaysConsumeMouseWheel) {
531
consumeMouseWheel = true;
532
}
533
if (!consumeMouseWheel && this._options.consumeMouseWheelIfScrollbarIsNeeded && (this._verticalScrollbar.isNeeded() || this._horizontalScrollbar.isNeeded())) {
534
consumeMouseWheel = true;
535
}
536
537
if (consumeMouseWheel) {
538
e.preventDefault();
539
e.stopPropagation();
540
}
541
}
542
543
private _onDidScroll(e: ScrollEvent): void {
544
this._shouldRender = this._horizontalScrollbar.onDidScroll(e) || this._shouldRender;
545
this._shouldRender = this._verticalScrollbar.onDidScroll(e) || this._shouldRender;
546
547
if (this._options.useShadows) {
548
this._shouldRender = true;
549
}
550
551
if (this._revealOnScroll) {
552
this._reveal();
553
}
554
555
if (!this._options.lazyRender) {
556
this._render();
557
}
558
}
559
560
/**
561
* Render / mutate the DOM now.
562
* Should be used together with the ctor option `lazyRender`.
563
*/
564
public renderNow(): void {
565
if (!this._options.lazyRender) {
566
throw new Error('Please use `lazyRender` together with `renderNow`!');
567
}
568
569
this._render();
570
}
571
572
private _render(): void {
573
if (!this._shouldRender) {
574
return;
575
}
576
577
this._shouldRender = false;
578
579
this._horizontalScrollbar.render();
580
this._verticalScrollbar.render();
581
582
if (this._options.useShadows) {
583
const scrollState = this._scrollable.getCurrentScrollPosition();
584
const enableTop = scrollState.scrollTop > 0;
585
const enableLeft = scrollState.scrollLeft > 0;
586
587
const leftClassName = (enableLeft ? ' left' : '');
588
const topClassName = (enableTop ? ' top' : '');
589
const topLeftClassName = (enableLeft || enableTop ? ' top-left-corner' : '');
590
this._leftShadowDomNode!.setClassName(`shadow${leftClassName}`);
591
this._topShadowDomNode!.setClassName(`shadow${topClassName}`);
592
this._topLeftShadowDomNode!.setClassName(`shadow${topLeftClassName}${topClassName}${leftClassName}`);
593
}
594
}
595
596
// -------------------- fade in / fade out --------------------
597
598
private _onDragStart(): void {
599
this._isDragging = true;
600
this._reveal();
601
}
602
603
private _onDragEnd(): void {
604
this._isDragging = false;
605
this._hide();
606
}
607
608
private _onMouseLeave(e: IMouseEvent): void {
609
this._mouseIsOver = false;
610
this._hide();
611
}
612
613
private _onMouseOver(e: IMouseEvent): void {
614
this._mouseIsOver = true;
615
this._reveal();
616
}
617
618
private _reveal(): void {
619
this._verticalScrollbar.beginReveal();
620
this._horizontalScrollbar.beginReveal();
621
this._scheduleHide();
622
}
623
624
private _hide(): void {
625
if (!this._mouseIsOver && !this._isDragging) {
626
this._verticalScrollbar.beginHide();
627
this._horizontalScrollbar.beginHide();
628
}
629
}
630
631
private _scheduleHide(): void {
632
if (!this._mouseIsOver && !this._isDragging) {
633
this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
634
}
635
}
636
}
637
638
export class ScrollableElement extends AbstractScrollableElement {
639
640
constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
641
options = options || {};
642
options.mouseWheelSmoothScroll = false;
643
const scrollable = new Scrollable({
644
forceIntegerValues: true,
645
smoothScrollDuration: 0,
646
scheduleAtNextAnimationFrame: (callback) => dom.scheduleAtNextAnimationFrame(dom.getWindow(element), callback)
647
});
648
super(element, options, scrollable);
649
this._register(scrollable);
650
}
651
652
public setScrollPosition(update: INewScrollPosition): void {
653
this._scrollable.setScrollPositionNow(update);
654
}
655
656
public getScrollPosition(): IScrollPosition {
657
return this._scrollable.getCurrentScrollPosition();
658
}
659
}
660
661
export class SmoothScrollableElement extends AbstractScrollableElement {
662
663
constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
664
super(element, options, scrollable);
665
}
666
667
public setScrollPosition(update: INewScrollPosition & { reuseAnimation?: boolean }): void {
668
if (update.reuseAnimation) {
669
this._scrollable.setScrollPositionSmooth(update, update.reuseAnimation);
670
} else {
671
this._scrollable.setScrollPositionNow(update);
672
}
673
}
674
675
public getScrollPosition(): IScrollPosition {
676
return this._scrollable.getCurrentScrollPosition();
677
}
678
679
}
680
681
export class DomScrollableElement extends AbstractScrollableElement {
682
683
private _element: HTMLElement;
684
685
constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
686
options = options || {};
687
options.mouseWheelSmoothScroll = false;
688
const scrollable = new Scrollable({
689
forceIntegerValues: false, // See https://github.com/microsoft/vscode/issues/139877
690
smoothScrollDuration: 0,
691
scheduleAtNextAnimationFrame: (callback) => dom.scheduleAtNextAnimationFrame(dom.getWindow(element), callback)
692
});
693
super(element, options, scrollable);
694
this._register(scrollable);
695
this._element = element;
696
this._register(this.onScroll((e) => {
697
if (e.scrollTopChanged) {
698
this._element.scrollTop = e.scrollTop;
699
}
700
if (e.scrollLeftChanged) {
701
this._element.scrollLeft = e.scrollLeft;
702
}
703
}));
704
this.scanDomNode();
705
}
706
707
public setScrollPosition(update: INewScrollPosition): void {
708
this._scrollable.setScrollPositionNow(update);
709
}
710
711
public getScrollPosition(): IScrollPosition {
712
return this._scrollable.getCurrentScrollPosition();
713
}
714
715
public scanDomNode(): void {
716
// width, scrollLeft, scrollWidth, height, scrollTop, scrollHeight
717
this.setScrollDimensions({
718
width: this._element.clientWidth,
719
scrollWidth: this._element.scrollWidth,
720
height: this._element.clientHeight,
721
scrollHeight: this._element.scrollHeight
722
});
723
this.setScrollPosition({
724
scrollLeft: this._element.scrollLeft,
725
scrollTop: this._element.scrollTop,
726
});
727
}
728
}
729
730
function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableElementResolvedOptions {
731
const result: ScrollableElementResolvedOptions = {
732
lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
733
className: (typeof opts.className !== 'undefined' ? opts.className : ''),
734
useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
735
handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
736
flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
737
consumeMouseWheelIfScrollbarIsNeeded: (typeof opts.consumeMouseWheelIfScrollbarIsNeeded !== 'undefined' ? opts.consumeMouseWheelIfScrollbarIsNeeded : false),
738
alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
739
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
740
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
741
fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
742
scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
743
mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
744
inertialScroll: (typeof opts.inertialScroll !== 'undefined' ? opts.inertialScroll : false),
745
arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
746
747
listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
748
749
horizontal: (typeof opts.horizontal !== 'undefined' ? opts.horizontal : ScrollbarVisibility.Auto),
750
horizontalScrollbarSize: (typeof opts.horizontalScrollbarSize !== 'undefined' ? opts.horizontalScrollbarSize : 10),
751
horizontalSliderSize: (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : 0),
752
horizontalHasArrows: (typeof opts.horizontalHasArrows !== 'undefined' ? opts.horizontalHasArrows : false),
753
754
vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : ScrollbarVisibility.Auto),
755
verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10),
756
verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false),
757
verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0),
758
759
scrollByPage: (typeof opts.scrollByPage !== 'undefined' ? opts.scrollByPage : false)
760
};
761
762
result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize);
763
result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize);
764
765
// Defaults are different on Macs
766
if (platform.isMacintosh) {
767
result.className += ' mac';
768
}
769
770
return result;
771
}
772
773