diff --git a/src/vs/editor/browser/controller/dragScrolling.ts b/src/vs/editor/browser/controller/dragScrolling.ts new file mode 100644 index 0000000000000..830d4b5ec5d9a --- /dev/null +++ b/src/vs/editor/browser/controller/dragScrolling.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../base/browser/dom.js'; +import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; +import { EditorOption } from '../../common/config/editorOptions.js'; +import { Position } from '../../common/core/position.js'; +import { ViewContext } from '../../common/viewModel/viewContext.js'; +import { NavigationCommandRevealType } from '../coreCommands.js'; +import { IMouseTarget, IMouseTargetOutsideEditor } from '../editorBrowser.js'; +import { createCoordinatesRelativeToEditor, createEditorPagePosition, EditorMouseEvent, PageCoordinates } from '../editorDom.js'; +import { IPointerHandlerHelper } from './mouseHandler.js'; +import { MouseTarget, MouseTargetFactory } from './mouseTarget.js'; + +export abstract class DragScrolling extends Disposable { + + private _operation: DragScrollingOperation | null; + + constructor( + protected readonly _context: ViewContext, + protected readonly _viewHelper: IPointerHandlerHelper, + protected readonly _mouseTargetFactory: MouseTargetFactory, + protected readonly _dispatchMouse: (position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType) => void + ) { + super(); + this._operation = null; + } + + public override dispose(): void { + super.dispose(); + this.stop(); + } + + public start(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): void { + if (this._operation) { + this._operation.setPosition(position, mouseEvent); + } else { + this._operation = this._createDragScrollingOperation(position, mouseEvent); + } + } + + public stop(): void { + if (this._operation) { + this._operation.dispose(); + this._operation = null; + } + } + + protected abstract _createDragScrollingOperation(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): DragScrollingOperation; +} + +export abstract class DragScrollingOperation extends Disposable { + + protected _position: IMouseTargetOutsideEditor; + protected _mouseEvent: EditorMouseEvent; + private _lastTime: number; + protected _animationFrameDisposable: IDisposable; + + constructor( + protected readonly _context: ViewContext, + protected readonly _viewHelper: IPointerHandlerHelper, + protected readonly _mouseTargetFactory: MouseTargetFactory, + protected readonly _dispatchMouse: (position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType) => void, + position: IMouseTargetOutsideEditor, + mouseEvent: EditorMouseEvent + ) { + super(); + this._position = position; + this._mouseEvent = mouseEvent; + this._lastTime = Date.now(); + this._animationFrameDisposable = dom.scheduleAtNextAnimationFrame(dom.getWindow(mouseEvent.browserEvent), () => this._execute()); + } + + public override dispose(): void { + this._animationFrameDisposable.dispose(); + super.dispose(); + } + + public setPosition(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): void { + this._position = position; + this._mouseEvent = mouseEvent; + } + + /** + * update internal state and return elapsed ms since last time + */ + protected _tick(): number { + const now = Date.now(); + const elapsed = now - this._lastTime; + this._lastTime = now; + return elapsed; + } + + protected abstract _execute(): void; + +} + +export class TopBottomDragScrolling extends DragScrolling { + protected _createDragScrollingOperation(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): DragScrollingOperation { + return new TopBottomDragScrollingOperation(this._context, this._viewHelper, this._mouseTargetFactory, this._dispatchMouse, position, mouseEvent); + } +} + +export class TopBottomDragScrollingOperation extends DragScrollingOperation { + + /** + * get the number of lines per second to auto-scroll + */ + private _getScrollSpeed(): number { + const lineHeight = this._context.configuration.options.get(EditorOption.lineHeight); + const viewportInLines = this._context.configuration.options.get(EditorOption.layoutInfo).height / lineHeight; + const outsideDistanceInLines = this._position.outsideDistance / lineHeight; + + if (outsideDistanceInLines <= 1.5) { + return Math.max(30, viewportInLines * (1 + outsideDistanceInLines)); + } + if (outsideDistanceInLines <= 3) { + return Math.max(60, viewportInLines * (2 + outsideDistanceInLines)); + } + return Math.max(200, viewportInLines * (7 + outsideDistanceInLines)); + } + + protected _execute(): void { + const lineHeight = this._context.configuration.options.get(EditorOption.lineHeight); + const scrollSpeedInLines = this._getScrollSpeed(); + const elapsed = this._tick(); + const scrollInPixels = scrollSpeedInLines * (elapsed / 1000) * lineHeight; + const scrollValue = (this._position.outsidePosition === 'above' ? -scrollInPixels : scrollInPixels); + + this._context.viewModel.viewLayout.deltaScrollNow(0, scrollValue); + this._viewHelper.renderNow(); + + const viewportData = this._context.viewLayout.getLinesViewportData(); + const edgeLineNumber = (this._position.outsidePosition === 'above' ? viewportData.startLineNumber : viewportData.endLineNumber); + + // First, try to find a position that matches the horizontal position of the mouse + let mouseTarget: IMouseTarget; + { + const editorPos = createEditorPagePosition(this._viewHelper.viewDomNode); + const horizontalScrollbarHeight = this._context.configuration.options.get(EditorOption.layoutInfo).horizontalScrollbarHeight; + const pos = new PageCoordinates(this._mouseEvent.pos.x, editorPos.y + editorPos.height - horizontalScrollbarHeight - 0.1); + const relativePos = createCoordinatesRelativeToEditor(this._viewHelper.viewDomNode, editorPos, pos); + mouseTarget = this._mouseTargetFactory.createMouseTarget(this._viewHelper.getLastRenderData(), editorPos, pos, relativePos, null); + } + if (!mouseTarget.position || mouseTarget.position.lineNumber !== edgeLineNumber) { + if (this._position.outsidePosition === 'above') { + mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, 1), 'above', this._position.outsideDistance); + } else { + mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, this._context.viewModel.getLineMaxColumn(edgeLineNumber)), 'below', this._position.outsideDistance); + } + } + + this._dispatchMouse(mouseTarget, true, NavigationCommandRevealType.None); + this._animationFrameDisposable = dom.scheduleAtNextAnimationFrame(dom.getWindow(mouseTarget.element), () => this._execute()); + } +} + +export class LeftRightDragScrolling extends DragScrolling { + protected _createDragScrollingOperation(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): DragScrollingOperation { + return new LeftRightDragScrollingOperation(this._context, this._viewHelper, this._mouseTargetFactory, this._dispatchMouse, position, mouseEvent); + } +} + +export class LeftRightDragScrollingOperation extends DragScrollingOperation { + + /** + * get the number of cols per second to auto-scroll + */ + private _getScrollSpeed(): number { + const charWidth = this._context.configuration.options.get(EditorOption.fontInfo).typicalFullwidthCharacterWidth; + const viewportInChars = this._context.configuration.options.get(EditorOption.layoutInfo).contentWidth / charWidth; + const outsideDistanceInChars = this._position.outsideDistance / charWidth; + if (outsideDistanceInChars <= 1.5) { + return Math.max(30, viewportInChars * (1 + outsideDistanceInChars)); + } + if (outsideDistanceInChars <= 3) { + return Math.max(60, viewportInChars * (2 + outsideDistanceInChars)); + } + return Math.max(200, viewportInChars * (7 + outsideDistanceInChars)); + } + + protected _execute(): void { + const charWidth = this._context.configuration.options.get(EditorOption.fontInfo).typicalFullwidthCharacterWidth; + const scrollSpeedInChars = this._getScrollSpeed(); + const elapsed = this._tick(); + const scrollInPixels = scrollSpeedInChars * (elapsed / 1000) * charWidth * 0.5; + const scrollValue = (this._position.outsidePosition === 'left' ? -scrollInPixels : scrollInPixels); + + this._context.viewModel.viewLayout.deltaScrollNow(scrollValue, 0); + this._viewHelper.renderNow(); + + if (!this._position.position) { + return; + } + const edgeLineNumber = this._position.position.lineNumber; + + // First, try to find a position that matches the horizontal position of the mouse + let mouseTarget: IMouseTarget; + { + const editorPos = createEditorPagePosition(this._viewHelper.viewDomNode); + const horizontalScrollbarHeight = this._context.configuration.options.get(EditorOption.layoutInfo).horizontalScrollbarHeight; + const pos = new PageCoordinates(this._mouseEvent.pos.x, editorPos.y + editorPos.height - horizontalScrollbarHeight - 0.1); + const relativePos = createCoordinatesRelativeToEditor(this._viewHelper.viewDomNode, editorPos, pos); + mouseTarget = this._mouseTargetFactory.createMouseTarget(this._viewHelper.getLastRenderData(), editorPos, pos, relativePos, null); + } + + if (this._position.outsidePosition === 'left') { + mouseTarget = MouseTarget.createOutsideEditor(mouseTarget.mouseColumn, new Position(edgeLineNumber, mouseTarget.mouseColumn), 'left', this._position.outsideDistance); + } else { + mouseTarget = MouseTarget.createOutsideEditor(mouseTarget.mouseColumn, new Position(edgeLineNumber, mouseTarget.mouseColumn), 'right', this._position.outsideDistance); + } + + this._dispatchMouse(mouseTarget, true, NavigationCommandRevealType.None); + this._animationFrameDisposable = dom.scheduleAtNextAnimationFrame(dom.getWindow(mouseTarget.element), () => this._execute()); + } +} diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index b1e326d835ac6..23ffda0bbd456 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -8,8 +8,8 @@ import { StandardWheelEvent, IMouseWheelEvent } from '../../../base/browser/mous import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; import * as platform from '../../../base/common/platform.js'; import { HitTestContext, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData } from './mouseTarget.js'; -import { IMouseTarget, IMouseTargetOutsideEditor, IMouseTargetViewZoneData, MouseTargetType } from '../editorBrowser.js'; -import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorPointerMoveMonitor, createEditorPagePosition, createCoordinatesRelativeToEditor, PageCoordinates } from '../editorDom.js'; +import { IMouseTarget, IMouseTargetViewZoneData, MouseTargetType } from '../editorBrowser.js'; +import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorPointerMoveMonitor, createEditorPagePosition, createCoordinatesRelativeToEditor } from '../editorDom.js'; import { ViewController } from '../view/viewController.js'; import { EditorZoom } from '../../common/config/editorZoom.js'; import { Position } from '../../common/core/position.js'; @@ -22,6 +22,7 @@ import { EditorOption } from '../../common/config/editorOptions.js'; import { NavigationCommandRevealType } from '../coreCommands.js'; import { MouseWheelClassifier } from '../../../base/browser/ui/scrollbar/scrollableElement.js'; import type { ViewLinesGpu } from '../viewParts/viewLinesGpu/viewLinesGpu.js'; +import { TopBottomDragScrolling, LeftRightDragScrolling } from './dragScrolling.js'; export interface IPointerHandlerHelper { viewDomNode: HTMLElement; @@ -361,6 +362,7 @@ class MouseDownOperation extends Disposable { private readonly _mouseMoveMonitor: GlobalEditorPointerMoveMonitor; private readonly _topBottomDragScrolling: TopBottomDragScrolling; + private readonly _leftRightDragScrolling: LeftRightDragScrolling; private readonly _mouseState: MouseDownState; private _currentSelection: Selection; @@ -386,6 +388,12 @@ class MouseDownOperation extends Disposable { this._mouseTargetFactory, (position, inSelectionMode, revealType) => this._dispatchMouse(position, inSelectionMode, revealType) )); + this._leftRightDragScrolling = this._register(new LeftRightDragScrolling( + this._context, + this._viewHelper, + this._mouseTargetFactory, + (position, inSelectionMode, revealType) => this._dispatchMouse(position, inSelectionMode, revealType) + )); this._mouseState = new MouseDownState(); this._currentSelection = new Selection(1, 1, 1, 1); @@ -417,10 +425,17 @@ class MouseDownOperation extends Disposable { target: position }); } else { - if (position.type === MouseTargetType.OUTSIDE_EDITOR && (position.outsidePosition === 'above' || position.outsidePosition === 'below')) { - this._topBottomDragScrolling.start(position, e); + if (position.type === MouseTargetType.OUTSIDE_EDITOR) { + if (position.outsidePosition === 'above' || position.outsidePosition === 'below') { + this._topBottomDragScrolling.start(position, e); + this._leftRightDragScrolling.stop(); + } else { + this._leftRightDragScrolling.start(position, e); + this._topBottomDragScrolling.stop(); + } } else { this._topBottomDragScrolling.stop(); + this._leftRightDragScrolling.stop(); this._dispatchMouse(position, true, NavigationCommandRevealType.Minimal); } } @@ -501,6 +516,7 @@ class MouseDownOperation extends Disposable { private _stop(): void { this._isActive = false; this._topBottomDragScrolling.stop(); + this._leftRightDragScrolling.stop(); } public onHeightChanged(): void { @@ -554,13 +570,23 @@ class MouseDownOperation extends Disposable { const possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + e.relativePos.y); - if (e.posx < editorContent.x) { - const outsideDistance = editorContent.x - e.posx; + const horizontalScrollPadding = 10; + const layoutInfo = this._context.configuration.options.get(EditorOption.layoutInfo); + + const xLeftBoundary = layoutInfo.contentLeft + horizontalScrollPadding; + if (e.relativePos.x <= xLeftBoundary) { + const outsideDistance = xLeftBoundary - e.relativePos.x; return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, 1), 'left', outsideDistance); } - if (e.posx > editorContent.x + editorContent.width) { - const outsideDistance = e.posx - editorContent.x - editorContent.width; + const contentRight = ( + layoutInfo.minimap.minimapLeft === 0 + ? layoutInfo.width - layoutInfo.verticalScrollbarWidth // Happens when minimap is hidden + : layoutInfo.minimap.minimapLeft + ); + const xRightBoundary = contentRight - horizontalScrollPadding; + if (e.relativePos.x >= xRightBoundary) { + const outsideDistance = e.relativePos.x - xRightBoundary; return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber)), 'right', outsideDistance); } @@ -630,135 +656,6 @@ class MouseDownOperation extends Disposable { } } -class TopBottomDragScrolling extends Disposable { - - private _operation: TopBottomDragScrollingOperation | null; - - constructor( - private readonly _context: ViewContext, - private readonly _viewHelper: IPointerHandlerHelper, - private readonly _mouseTargetFactory: MouseTargetFactory, - private readonly _dispatchMouse: (position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType) => void, - ) { - super(); - this._operation = null; - } - - public override dispose(): void { - super.dispose(); - this.stop(); - } - - public start(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): void { - if (this._operation) { - this._operation.setPosition(position, mouseEvent); - } else { - this._operation = new TopBottomDragScrollingOperation(this._context, this._viewHelper, this._mouseTargetFactory, this._dispatchMouse, position, mouseEvent); - } - } - - public stop(): void { - if (this._operation) { - this._operation.dispose(); - this._operation = null; - } - } -} - -class TopBottomDragScrollingOperation extends Disposable { - - private _position: IMouseTargetOutsideEditor; - private _mouseEvent: EditorMouseEvent; - private _lastTime: number; - private _animationFrameDisposable: IDisposable; - - constructor( - private readonly _context: ViewContext, - private readonly _viewHelper: IPointerHandlerHelper, - private readonly _mouseTargetFactory: MouseTargetFactory, - private readonly _dispatchMouse: (position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType) => void, - position: IMouseTargetOutsideEditor, - mouseEvent: EditorMouseEvent - ) { - super(); - this._position = position; - this._mouseEvent = mouseEvent; - this._lastTime = Date.now(); - this._animationFrameDisposable = dom.scheduleAtNextAnimationFrame(dom.getWindow(mouseEvent.browserEvent), () => this._execute()); - } - - public override dispose(): void { - this._animationFrameDisposable.dispose(); - super.dispose(); - } - - public setPosition(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): void { - this._position = position; - this._mouseEvent = mouseEvent; - } - - /** - * update internal state and return elapsed ms since last time - */ - private _tick(): number { - const now = Date.now(); - const elapsed = now - this._lastTime; - this._lastTime = now; - return elapsed; - } - - /** - * get the number of lines per second to auto-scroll - */ - private _getScrollSpeed(): number { - const lineHeight = this._context.configuration.options.get(EditorOption.lineHeight); - const viewportInLines = this._context.configuration.options.get(EditorOption.layoutInfo).height / lineHeight; - const outsideDistanceInLines = this._position.outsideDistance / lineHeight; - - if (outsideDistanceInLines <= 1.5) { - return Math.max(30, viewportInLines * (1 + outsideDistanceInLines)); - } - if (outsideDistanceInLines <= 3) { - return Math.max(60, viewportInLines * (2 + outsideDistanceInLines)); - } - return Math.max(200, viewportInLines * (7 + outsideDistanceInLines)); - } - - private _execute(): void { - const lineHeight = this._context.configuration.options.get(EditorOption.lineHeight); - const scrollSpeedInLines = this._getScrollSpeed(); - const elapsed = this._tick(); - const scrollInPixels = scrollSpeedInLines * (elapsed / 1000) * lineHeight; - const scrollValue = (this._position.outsidePosition === 'above' ? -scrollInPixels : scrollInPixels); - - this._context.viewModel.viewLayout.deltaScrollNow(0, scrollValue); - this._viewHelper.renderNow(); - - const viewportData = this._context.viewLayout.getLinesViewportData(); - const edgeLineNumber = (this._position.outsidePosition === 'above' ? viewportData.startLineNumber : viewportData.endLineNumber); - - // First, try to find a position that matches the horizontal position of the mouse - let mouseTarget: IMouseTarget; - { - const editorPos = createEditorPagePosition(this._viewHelper.viewDomNode); - const horizontalScrollbarHeight = this._context.configuration.options.get(EditorOption.layoutInfo).horizontalScrollbarHeight; - const pos = new PageCoordinates(this._mouseEvent.pos.x, editorPos.y + editorPos.height - horizontalScrollbarHeight - 0.1); - const relativePos = createCoordinatesRelativeToEditor(this._viewHelper.viewDomNode, editorPos, pos); - mouseTarget = this._mouseTargetFactory.createMouseTarget(this._viewHelper.getLastRenderData(), editorPos, pos, relativePos, null); - } - if (!mouseTarget.position || mouseTarget.position.lineNumber !== edgeLineNumber) { - if (this._position.outsidePosition === 'above') { - mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, 1), 'above', this._position.outsideDistance); - } else { - mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, this._context.viewModel.getLineMaxColumn(edgeLineNumber)), 'below', this._position.outsideDistance); - } - } - - this._dispatchMouse(mouseTarget, true, NavigationCommandRevealType.None); - this._animationFrameDisposable = dom.scheduleAtNextAnimationFrame(dom.getWindow(mouseTarget.element), () => this._execute()); - } -} - class MouseDownState { private static readonly CLEAR_MOUSE_DOWN_COUNT_TIME = 400; // ms