Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/middleScroll/browser/middleScrollController.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 { getWindow, addDisposableListener, n } from '../../../../base/browser/dom.js';
7
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
8
import { ICodeEditor } from '../../../browser/editorBrowser.js';
9
import { IEditorContribution, INewScrollPosition } from '../../../common/editorCommon.js';
10
import { EditorOption } from '../../../common/config/editorOptions.js';
11
import { autorun, derived, disposableObservableValue, IObservable, observableValue } from '../../../../base/common/observable.js';
12
import { observableCodeEditor } from '../../../browser/observableCodeEditor.js';
13
import { Point } from '../../../common/core/2d/point.js';
14
import { AnimationFrameScheduler } from '../../inlineCompletions/browser/model/animation.js';
15
import { appendRemoveOnDispose } from '../../../browser/widget/diffEditor/utils.js';
16
import './middleScroll.css';
17
18
export class MiddleScrollController extends Disposable implements IEditorContribution {
19
public static readonly ID = 'editor.contrib.middleScroll';
20
21
static get(editor: ICodeEditor): MiddleScrollController | null {
22
return editor.getContribution<MiddleScrollController>(MiddleScrollController.ID);
23
}
24
25
constructor(
26
private readonly _editor: ICodeEditor
27
) {
28
super();
29
30
const obsEditor = observableCodeEditor(this._editor);
31
const scrollOnMiddleClick = obsEditor.getOption(EditorOption.scrollOnMiddleClick);
32
33
this._register(autorun(reader => {
34
if (!scrollOnMiddleClick.read(reader)) {
35
return;
36
}
37
const editorDomNode = obsEditor.domNode.read(reader);
38
if (!editorDomNode) {
39
return;
40
}
41
42
const scrollingSession = reader.store.add(
43
disposableObservableValue(
44
'scrollingSession',
45
undefined as undefined | { mouseDeltaAfterThreshold: IObservable<Point>; initialMousePosInEditor: Point; didScroll: boolean } & IDisposable
46
)
47
);
48
49
reader.store.add(this._editor.onMouseDown(e => {
50
const session = scrollingSession.get();
51
if (session) {
52
scrollingSession.set(undefined, undefined);
53
return;
54
}
55
56
if (!e.event.middleButton) {
57
return;
58
}
59
e.event.stopPropagation();
60
e.event.preventDefault();
61
62
const store = new DisposableStore();
63
const initialPos = new Point(e.event.posx, e.event.posy);
64
const mousePos = observeWindowMousePos(getWindow(editorDomNode), initialPos, store);
65
const mouseDeltaAfterThreshold = mousePos.map(v => v.subtract(initialPos).withThreshold(5));
66
67
const editorDomNodeRect = editorDomNode.getBoundingClientRect();
68
const initialMousePosInEditor = new Point(initialPos.x - editorDomNodeRect.left, initialPos.y - editorDomNodeRect.top);
69
70
scrollingSession.set({
71
mouseDeltaAfterThreshold,
72
initialMousePosInEditor,
73
didScroll: false,
74
dispose: () => store.dispose(),
75
}, undefined);
76
77
store.add(this._editor.onMouseUp(e => {
78
const session = scrollingSession.get();
79
if (session && session.didScroll) {
80
// Only cancel session on release if the user scrolled during it
81
scrollingSession.set(undefined, undefined);
82
}
83
}));
84
85
store.add(this._editor.onKeyDown(e => {
86
scrollingSession.set(undefined, undefined);
87
}));
88
}));
89
90
reader.store.add(autorun(reader => {
91
const session = scrollingSession.read(reader);
92
if (!session) {
93
return;
94
}
95
96
let lastTime = Date.now();
97
reader.store.add(autorun(reader => {
98
AnimationFrameScheduler.instance.invalidateOnNextAnimationFrame(reader);
99
100
const curTime = Date.now();
101
const frameDurationMs = curTime - lastTime;
102
lastTime = curTime;
103
104
const mouseDelta = session.mouseDeltaAfterThreshold.get();
105
106
// scroll by mouse delta every 32ms
107
const factor = frameDurationMs / 32;
108
const scrollDelta = mouseDelta.scale(factor);
109
110
const scrollPos = new Point(this._editor.getScrollLeft(), this._editor.getScrollTop());
111
this._editor.setScrollPosition(toScrollPosition(scrollPos.add(scrollDelta)));
112
if (!scrollDelta.isZero()) {
113
session.didScroll = true;
114
}
115
}));
116
117
const directionAttr = derived(reader => {
118
const delta = session.mouseDeltaAfterThreshold.read(reader);
119
let direction: string = '';
120
direction += (delta.y < 0 ? 'n' : (delta.y > 0 ? 's' : ''));
121
direction += (delta.x < 0 ? 'w' : (delta.x > 0 ? 'e' : ''));
122
return direction;
123
});
124
reader.store.add(autorun(reader => {
125
editorDomNode.setAttribute('data-scroll-direction', directionAttr.read(reader));
126
}));
127
}));
128
129
const dotDomElem = reader.store.add(n.div({
130
class: ['scroll-editor-on-middle-click-dot', scrollingSession.map(session => session ? '' : 'hidden')],
131
style: {
132
left: scrollingSession.map((session) => session ? session.initialMousePosInEditor.x : 0),
133
top: scrollingSession.map((session) => session ? session.initialMousePosInEditor.y : 0),
134
}
135
}).toDisposableLiveElement());
136
reader.store.add(appendRemoveOnDispose(editorDomNode, dotDomElem.element));
137
138
reader.store.add(autorun(reader => {
139
const session = scrollingSession.read(reader);
140
editorDomNode.classList.toggle('scroll-editor-on-middle-click-editor', !!session);
141
}));
142
}));
143
}
144
}
145
146
function observeWindowMousePos(window: Window, initialPos: Point, store: DisposableStore): IObservable<Point> {
147
const val = observableValue('pos', initialPos);
148
store.add(addDisposableListener(window, 'mousemove', (e: MouseEvent) => {
149
val.set(new Point(e.pageX, e.pageY), undefined);
150
}));
151
return val;
152
}
153
154
function toScrollPosition(p: Point): INewScrollPosition {
155
return {
156
scrollLeft: p.x,
157
scrollTop: p.y,
158
};
159
}
160
161