Path: blob/main/src/vs/editor/contrib/middleScroll/browser/middleScrollController.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { getWindow, addDisposableListener, n } from '../../../../base/browser/dom.js';6import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';7import { ICodeEditor } from '../../../browser/editorBrowser.js';8import { IEditorContribution, INewScrollPosition } from '../../../common/editorCommon.js';9import { EditorOption } from '../../../common/config/editorOptions.js';10import { autorun, derived, disposableObservableValue, IObservable, observableValue } from '../../../../base/common/observable.js';11import { observableCodeEditor } from '../../../browser/observableCodeEditor.js';12import { Point } from '../../../common/core/2d/point.js';13import { AnimationFrameScheduler } from '../../inlineCompletions/browser/model/animation.js';14import { appendRemoveOnDispose } from '../../../browser/widget/diffEditor/utils.js';15import './middleScroll.css';1617export class MiddleScrollController extends Disposable implements IEditorContribution {18public static readonly ID = 'editor.contrib.middleScroll';1920static get(editor: ICodeEditor): MiddleScrollController | null {21return editor.getContribution<MiddleScrollController>(MiddleScrollController.ID);22}2324constructor(25private readonly _editor: ICodeEditor26) {27super();2829const obsEditor = observableCodeEditor(this._editor);30const scrollOnMiddleClick = obsEditor.getOption(EditorOption.scrollOnMiddleClick);3132this._register(autorun(reader => {33if (!scrollOnMiddleClick.read(reader)) {34return;35}36const editorDomNode = obsEditor.domNode.read(reader);37if (!editorDomNode) {38return;39}4041const scrollingSession = reader.store.add(42disposableObservableValue(43'scrollingSession',44undefined as undefined | { mouseDeltaAfterThreshold: IObservable<Point>; initialMousePosInEditor: Point; didScroll: boolean } & IDisposable45)46);4748reader.store.add(this._editor.onMouseDown(e => {49const session = scrollingSession.get();50if (session) {51scrollingSession.set(undefined, undefined);52return;53}5455if (!e.event.middleButton) {56return;57}58e.event.stopPropagation();59e.event.preventDefault();6061const store = new DisposableStore();62const initialPos = new Point(e.event.posx, e.event.posy);63const mousePos = observeWindowMousePos(getWindow(editorDomNode), initialPos, store);64const mouseDeltaAfterThreshold = mousePos.map(v => v.subtract(initialPos).withThreshold(5));6566const editorDomNodeRect = editorDomNode.getBoundingClientRect();67const initialMousePosInEditor = new Point(initialPos.x - editorDomNodeRect.left, initialPos.y - editorDomNodeRect.top);6869scrollingSession.set({70mouseDeltaAfterThreshold,71initialMousePosInEditor,72didScroll: false,73dispose: () => store.dispose(),74}, undefined);7576store.add(this._editor.onMouseUp(e => {77const session = scrollingSession.get();78if (session && session.didScroll) {79// Only cancel session on release if the user scrolled during it80scrollingSession.set(undefined, undefined);81}82}));8384store.add(this._editor.onKeyDown(e => {85scrollingSession.set(undefined, undefined);86}));87}));8889reader.store.add(autorun(reader => {90const session = scrollingSession.read(reader);91if (!session) {92return;93}9495let lastTime = Date.now();96reader.store.add(autorun(reader => {97AnimationFrameScheduler.instance.invalidateOnNextAnimationFrame(reader);9899const curTime = Date.now();100const frameDurationMs = curTime - lastTime;101lastTime = curTime;102103const mouseDelta = session.mouseDeltaAfterThreshold.get();104105// scroll by mouse delta every 32ms106const factor = frameDurationMs / 32;107const scrollDelta = mouseDelta.scale(factor);108109const scrollPos = new Point(this._editor.getScrollLeft(), this._editor.getScrollTop());110this._editor.setScrollPosition(toScrollPosition(scrollPos.add(scrollDelta)));111if (!scrollDelta.isZero()) {112session.didScroll = true;113}114}));115116const directionAttr = derived(reader => {117const delta = session.mouseDeltaAfterThreshold.read(reader);118let direction: string = '';119direction += (delta.y < 0 ? 'n' : (delta.y > 0 ? 's' : ''));120direction += (delta.x < 0 ? 'w' : (delta.x > 0 ? 'e' : ''));121return direction;122});123reader.store.add(autorun(reader => {124editorDomNode.setAttribute('data-scroll-direction', directionAttr.read(reader));125}));126}));127128const dotDomElem = reader.store.add(n.div({129class: ['scroll-editor-on-middle-click-dot', scrollingSession.map(session => session ? '' : 'hidden')],130style: {131left: scrollingSession.map((session) => session ? session.initialMousePosInEditor.x : 0),132top: scrollingSession.map((session) => session ? session.initialMousePosInEditor.y : 0),133}134}).toDisposableLiveElement());135reader.store.add(appendRemoveOnDispose(editorDomNode, dotDomElem.element));136137reader.store.add(autorun(reader => {138const session = scrollingSession.read(reader);139editorDomNode.classList.toggle('scroll-editor-on-middle-click-editor', !!session);140}));141}));142}143}144145function observeWindowMousePos(window: Window, initialPos: Point, store: DisposableStore): IObservable<Point> {146const val = observableValue('pos', initialPos);147store.add(addDisposableListener(window, 'mousemove', (e: MouseEvent) => {148val.set(new Point(e.pageX, e.pageY), undefined);149}));150return val;151}152153function toScrollPosition(p: Point): INewScrollPosition {154return {155scrollLeft: p.x,156scrollTop: p.y,157};158}159160161