Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/scrollbar/abstractScrollbar.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 * as dom from '../../dom.js';
7
import { createFastDomNode, FastDomNode } from '../../fastDomNode.js';
8
import { GlobalPointerMoveMonitor } from '../../globalPointerMoveMonitor.js';
9
import { StandardWheelEvent } from '../../mouseEvent.js';
10
import { ScrollbarArrow, ScrollbarArrowOptions } from './scrollbarArrow.js';
11
import { ScrollbarState } from './scrollbarState.js';
12
import { ScrollbarVisibilityController } from './scrollbarVisibilityController.js';
13
import { Widget } from '../widget.js';
14
import * as platform from '../../../common/platform.js';
15
import { INewScrollPosition, Scrollable, ScrollbarVisibility } from '../../../common/scrollable.js';
16
17
/**
18
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
19
*/
20
const POINTER_DRAG_RESET_DISTANCE = 140;
21
22
export interface ISimplifiedPointerEvent {
23
buttons: number;
24
pageX: number;
25
pageY: number;
26
}
27
28
export interface ScrollbarHost {
29
onMouseWheel(mouseWheelEvent: StandardWheelEvent): void;
30
onDragStart(): void;
31
onDragEnd(): void;
32
}
33
34
export interface AbstractScrollbarOptions {
35
lazyRender: boolean;
36
host: ScrollbarHost;
37
scrollbarState: ScrollbarState;
38
visibility: ScrollbarVisibility;
39
extraScrollbarClassName: string;
40
scrollable: Scrollable;
41
scrollByPage: boolean;
42
}
43
44
export abstract class AbstractScrollbar extends Widget {
45
46
protected _host: ScrollbarHost;
47
protected _scrollable: Scrollable;
48
protected _scrollByPage: boolean;
49
private _lazyRender: boolean;
50
protected _scrollbarState: ScrollbarState;
51
protected _visibilityController: ScrollbarVisibilityController;
52
private _pointerMoveMonitor: GlobalPointerMoveMonitor;
53
54
public domNode: FastDomNode<HTMLElement>;
55
public slider!: FastDomNode<HTMLElement>;
56
57
protected _shouldRender: boolean;
58
59
constructor(opts: AbstractScrollbarOptions) {
60
super();
61
this._lazyRender = opts.lazyRender;
62
this._host = opts.host;
63
this._scrollable = opts.scrollable;
64
this._scrollByPage = opts.scrollByPage;
65
this._scrollbarState = opts.scrollbarState;
66
this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName));
67
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
68
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
69
this._shouldRender = true;
70
this.domNode = createFastDomNode(document.createElement('div'));
71
this.domNode.setAttribute('role', 'presentation');
72
this.domNode.setAttribute('aria-hidden', 'true');
73
74
this._visibilityController.setDomNode(this.domNode);
75
this.domNode.setPosition('absolute');
76
77
this._register(dom.addDisposableListener(this.domNode.domNode, dom.EventType.POINTER_DOWN, (e: PointerEvent) => this._domNodePointerDown(e)));
78
}
79
80
// ----------------- creation
81
82
/**
83
* Creates the dom node for an arrow & adds it to the container
84
*/
85
protected _createArrow(opts: ScrollbarArrowOptions): void {
86
const arrow = this._register(new ScrollbarArrow(opts));
87
this.domNode.domNode.appendChild(arrow.bgDomNode);
88
this.domNode.domNode.appendChild(arrow.domNode);
89
}
90
91
/**
92
* Creates the slider dom node, adds it to the container & hooks up the events
93
*/
94
protected _createSlider(top: number, left: number, width: number | undefined, height: number | undefined): void {
95
this.slider = createFastDomNode(document.createElement('div'));
96
this.slider.setClassName('slider');
97
this.slider.setPosition('absolute');
98
this.slider.setTop(top);
99
this.slider.setLeft(left);
100
if (typeof width === 'number') {
101
this.slider.setWidth(width);
102
}
103
if (typeof height === 'number') {
104
this.slider.setHeight(height);
105
}
106
this.slider.setLayerHinting(true);
107
this.slider.setContain('strict');
108
109
this.domNode.domNode.appendChild(this.slider.domNode);
110
111
this._register(dom.addDisposableListener(
112
this.slider.domNode,
113
dom.EventType.POINTER_DOWN,
114
(e: PointerEvent) => {
115
if (e.button === 0) {
116
e.preventDefault();
117
this._sliderPointerDown(e);
118
}
119
}
120
));
121
122
this.onclick(this.slider.domNode, e => {
123
if (e.leftButton) {
124
e.stopPropagation();
125
}
126
});
127
}
128
129
// ----------------- Update state
130
131
protected _onElementSize(visibleSize: number): boolean {
132
if (this._scrollbarState.setVisibleSize(visibleSize)) {
133
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
134
this._shouldRender = true;
135
if (!this._lazyRender) {
136
this.render();
137
}
138
}
139
return this._shouldRender;
140
}
141
142
protected _onElementScrollSize(elementScrollSize: number): boolean {
143
if (this._scrollbarState.setScrollSize(elementScrollSize)) {
144
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
145
this._shouldRender = true;
146
if (!this._lazyRender) {
147
this.render();
148
}
149
}
150
return this._shouldRender;
151
}
152
153
protected _onElementScrollPosition(elementScrollPosition: number): boolean {
154
if (this._scrollbarState.setScrollPosition(elementScrollPosition)) {
155
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
156
this._shouldRender = true;
157
if (!this._lazyRender) {
158
this.render();
159
}
160
}
161
return this._shouldRender;
162
}
163
164
// ----------------- rendering
165
166
public beginReveal(): void {
167
this._visibilityController.setShouldBeVisible(true);
168
}
169
170
public beginHide(): void {
171
this._visibilityController.setShouldBeVisible(false);
172
}
173
174
public render(): void {
175
if (!this._shouldRender) {
176
return;
177
}
178
this._shouldRender = false;
179
180
this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize());
181
this._updateSlider(this._scrollbarState.getSliderSize(), this._scrollbarState.getArrowSize() + this._scrollbarState.getSliderPosition());
182
}
183
// ----------------- DOM events
184
185
private _domNodePointerDown(e: PointerEvent): void {
186
if (e.target !== this.domNode.domNode) {
187
return;
188
}
189
this._onPointerDown(e);
190
}
191
192
public delegatePointerDown(e: PointerEvent): void {
193
const domTop = this.domNode.domNode.getClientRects()[0].top;
194
const sliderStart = domTop + this._scrollbarState.getSliderPosition();
195
const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
196
const pointerPos = this._sliderPointerPosition(e);
197
if (sliderStart <= pointerPos && pointerPos <= sliderStop) {
198
// Act as if it was a pointer down on the slider
199
if (e.button === 0) {
200
e.preventDefault();
201
this._sliderPointerDown(e);
202
}
203
} else {
204
// Act as if it was a pointer down on the scrollbar
205
this._onPointerDown(e);
206
}
207
}
208
209
private _onPointerDown(e: PointerEvent): void {
210
let offsetX: number;
211
let offsetY: number;
212
if (e.target === this.domNode.domNode && typeof e.offsetX === 'number' && typeof e.offsetY === 'number') {
213
offsetX = e.offsetX;
214
offsetY = e.offsetY;
215
} else {
216
const domNodePosition = dom.getDomNodePagePosition(this.domNode.domNode);
217
offsetX = e.pageX - domNodePosition.left;
218
offsetY = e.pageY - domNodePosition.top;
219
}
220
221
const isMouse = (e.pointerType === 'mouse');
222
const isLeftClick = (e.button === 0);
223
224
if (isLeftClick || !isMouse) {
225
const offset = this._pointerDownRelativePosition(offsetX, offsetY);
226
this._setDesiredScrollPositionNow(
227
this._scrollByPage
228
? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset)
229
: this._scrollbarState.getDesiredScrollPositionFromOffset(offset)
230
);
231
}
232
233
if (isLeftClick) {
234
// left button
235
e.preventDefault();
236
this._sliderPointerDown(e);
237
}
238
}
239
240
private _sliderPointerDown(e: PointerEvent): void {
241
if (!e.target || !(e.target instanceof Element)) {
242
return;
243
}
244
const initialPointerPosition = this._sliderPointerPosition(e);
245
const initialPointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(e);
246
const initialScrollbarState = this._scrollbarState.clone();
247
this.slider.toggleClassName('active', true);
248
249
this._pointerMoveMonitor.startMonitoring(
250
e.target,
251
e.pointerId,
252
e.buttons,
253
(pointerMoveData: PointerEvent) => {
254
const pointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(pointerMoveData);
255
const pointerOrthogonalDelta = Math.abs(pointerOrthogonalPosition - initialPointerOrthogonalPosition);
256
257
if (platform.isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) {
258
// The pointer has wondered away from the scrollbar => reset dragging
259
this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition());
260
return;
261
}
262
263
const pointerPosition = this._sliderPointerPosition(pointerMoveData);
264
const pointerDelta = pointerPosition - initialPointerPosition;
265
this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(pointerDelta));
266
},
267
() => {
268
this.slider.toggleClassName('active', false);
269
this._host.onDragEnd();
270
}
271
);
272
273
this._host.onDragStart();
274
}
275
276
private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void {
277
278
const desiredScrollPosition: INewScrollPosition = {};
279
this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
280
281
this._scrollable.setScrollPositionNow(desiredScrollPosition);
282
}
283
284
public updateScrollbarSize(scrollbarSize: number): void {
285
this._updateScrollbarSize(scrollbarSize);
286
this._scrollbarState.setScrollbarSize(scrollbarSize);
287
this._shouldRender = true;
288
if (!this._lazyRender) {
289
this.render();
290
}
291
}
292
293
public isNeeded(): boolean {
294
return this._scrollbarState.isNeeded();
295
}
296
297
// ----------------- Overwrite these
298
299
protected abstract _renderDomNode(largeSize: number, smallSize: number): void;
300
protected abstract _updateSlider(sliderSize: number, sliderPosition: number): void;
301
302
protected abstract _pointerDownRelativePosition(offsetX: number, offsetY: number): number;
303
protected abstract _sliderPointerPosition(e: ISimplifiedPointerEvent): number;
304
protected abstract _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number;
305
protected abstract _updateScrollbarSize(size: number): void;
306
307
public abstract writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void;
308
}
309
310