Merge pull request #5757 from Tyriar/5754
Move bindMouse into MouseService
This commit is contained in:
@@ -85,7 +85,7 @@ export class CellInspectorWindow extends BaseWindow implements IControlWindow {
|
||||
|
||||
terminal.element.addEventListener('mousemove', (e: MouseEvent) => {
|
||||
const core = (terminal as any)._core;
|
||||
const coords = core._mouseService?.getCoords(e, core.screenElement, terminal.cols, terminal.rows);
|
||||
const coords = core._mouseCoordsService?.getCoords(e, core.screenElement, terminal.cols, terminal.rows);
|
||||
if (!coords) {
|
||||
this._clearDisplay();
|
||||
return;
|
||||
|
||||
@@ -36,16 +36,17 @@ import { CharSizeService } from 'browser/services/CharSizeService';
|
||||
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
|
||||
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
|
||||
import { LinkProviderService } from 'browser/services/LinkProviderService';
|
||||
import { MouseCoordsService } from 'browser/services/MouseCoordsService';
|
||||
import { MouseService } from 'browser/services/MouseService';
|
||||
import { RenderService } from 'browser/services/RenderService';
|
||||
import { SelectionService } from 'browser/services/SelectionService';
|
||||
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
import { KeyboardService } from 'browser/services/KeyboardService';
|
||||
import { channels, color, rgb } from 'common/Color';
|
||||
import { CoreTerminal } from 'common/CoreTerminal';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
|
||||
import { ColorRequestType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { C0, C1ESCAPED } from 'common/data/EscapeSequences';
|
||||
@@ -87,6 +88,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
// Optional browser services
|
||||
private _charSizeService: ICharSizeService | undefined;
|
||||
private _coreBrowserService: ICoreBrowserService | undefined;
|
||||
private _mouseCoordsService: IMouseCoordsService | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _themeService: IThemeService | undefined;
|
||||
@@ -543,8 +545,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
|
||||
this._helperContainer.appendChild(this._compositionView);
|
||||
|
||||
this._mouseService = this._instantiationService.createInstance(MouseService);
|
||||
this._instantiationService.setService(IMouseService, this._mouseService);
|
||||
this._mouseCoordsService = this._instantiationService.createInstance(MouseCoordsService);
|
||||
this._instantiationService.setService(IMouseCoordsService, this._mouseCoordsService);
|
||||
|
||||
const linkifier = this._linkifier.value = this._register(this._instantiationService.createInstance(Linkifier, this.screenElement));
|
||||
|
||||
@@ -579,6 +581,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
linkifier
|
||||
));
|
||||
this._instantiationService.setService(ISelectionService, this._selectionService);
|
||||
this._mouseService = this._instantiationService.createInstance(MouseService);
|
||||
this._mouseService.setCustomWheelEventHandler(this._customWheelEventHandler);
|
||||
this._instantiationService.setService(IMouseService, this._mouseService);
|
||||
this._register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
|
||||
this._register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
|
||||
this._register(this._selectionService.onRequestRedraw(e => this._renderService!.handleSelectionChanged(e.start, e.end, e.columnSelectMode)));
|
||||
@@ -638,285 +643,17 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
// Listen for mouse events and translate
|
||||
// them into terminal mouse protocols.
|
||||
this.bindMouse();
|
||||
this._mouseService.bindMouse({
|
||||
element: this.element!,
|
||||
screenElement: this.screenElement!,
|
||||
document: this._document!
|
||||
}, disposable => this._register(disposable), () => this.focus());
|
||||
}
|
||||
|
||||
private _createRenderer(): IRenderer {
|
||||
return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind certain mouse events to the terminal.
|
||||
* By default only 3 button + wheel up/down is ativated. For higher buttons
|
||||
* no mouse report will be created. Typically the standard actions will be active.
|
||||
*
|
||||
* There are several reasons not to enable support for higher buttons/wheel:
|
||||
* - Button 4 and 5 are typically used for history back and forward navigation,
|
||||
* there is no straight forward way to supress/intercept those standard actions.
|
||||
* - Support for higher buttons does not work in some platform/browser combinations.
|
||||
* - Left/right wheel was not tested.
|
||||
* - Emulators vary in mouse button support, typically only 3 buttons and
|
||||
* wheel up/down work reliable.
|
||||
*
|
||||
* TODO: Move mouse event code into its own file.
|
||||
*/
|
||||
public bindMouse(): void {
|
||||
const self = this;
|
||||
const el = this.element!;
|
||||
|
||||
// send event to CoreMouseService
|
||||
function sendEvent(ev: MouseEvent | WheelEvent): boolean {
|
||||
// Get mouse coordinates
|
||||
const pos = self._mouseService?.getMouseReportCoords(ev, self.screenElement!);
|
||||
if (!pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let but: CoreMouseButton;
|
||||
let action: CoreMouseAction | undefined;
|
||||
switch ((ev as any).overrideType || ev.type) {
|
||||
case 'mousemove':
|
||||
action = CoreMouseAction.MOVE;
|
||||
if (ev.buttons === undefined) {
|
||||
// buttons is not supported on macOS, try to get a value from button instead
|
||||
but = CoreMouseButton.NONE;
|
||||
if (ev.button !== undefined) {
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
}
|
||||
} else {
|
||||
// according to MDN buttons only reports up to button 5 (AUX2)
|
||||
but = ev.buttons & 1 ? CoreMouseButton.LEFT :
|
||||
ev.buttons & 4 ? CoreMouseButton.MIDDLE :
|
||||
ev.buttons & 2 ? CoreMouseButton.RIGHT :
|
||||
CoreMouseButton.NONE; // fallback to NONE
|
||||
}
|
||||
break;
|
||||
case 'mouseup':
|
||||
action = CoreMouseAction.UP;
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'mousedown':
|
||||
action = CoreMouseAction.DOWN;
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'wheel':
|
||||
if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) {
|
||||
return false;
|
||||
}
|
||||
const deltaY = (ev as WheelEvent).deltaY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
const lines = self.coreMouseService.consumeWheelEvent(
|
||||
ev as WheelEvent,
|
||||
self._renderService?.dimensions?.device?.cell?.height,
|
||||
self._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
return false;
|
||||
}
|
||||
action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
|
||||
but = CoreMouseButton.WHEEL;
|
||||
break;
|
||||
default:
|
||||
// dont handle other event types by accident
|
||||
return false;
|
||||
}
|
||||
|
||||
// exit if we cannot determine valid button/action values
|
||||
// do nothing for higher buttons than wheel
|
||||
if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self.coreMouseService.triggerMouseEvent({
|
||||
col: pos.col,
|
||||
row: pos.row,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
button: but,
|
||||
action,
|
||||
ctrl: ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
shift: ev.shiftKey
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener state handling.
|
||||
* We listen to the onProtocolChange event of CoreMouseService and put
|
||||
* requested listeners in `requestedEvents`. With this the listeners
|
||||
* have all bits to do the event listener juggling.
|
||||
* Note: 'mousedown' currently is "always on" and not managed
|
||||
* by onProtocolChange.
|
||||
*/
|
||||
const requestedEvents: { [key: string]: ((ev: MouseEvent | WheelEvent) => void) | null } = {
|
||||
mouseup: null,
|
||||
wheel: null,
|
||||
mousedrag: null,
|
||||
mousemove: null
|
||||
};
|
||||
const eventListeners: { [key: string]: (ev: any) => void | boolean } = {
|
||||
mouseup: (ev: MouseEvent) => {
|
||||
sendEvent(ev);
|
||||
if (!ev.buttons) {
|
||||
// if no other button is held remove global handlers
|
||||
this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
|
||||
if (requestedEvents.mousedrag) {
|
||||
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}
|
||||
},
|
||||
wheel: (ev: WheelEvent) => {
|
||||
sendEvent(ev);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
mousedrag: (ev: MouseEvent) => {
|
||||
// deal only with move while a button is held
|
||||
if (ev.buttons) {
|
||||
sendEvent(ev);
|
||||
}
|
||||
},
|
||||
mousemove: (ev: MouseEvent) => {
|
||||
// deal only with move without any button
|
||||
if (!ev.buttons) {
|
||||
sendEvent(ev);
|
||||
}
|
||||
}
|
||||
};
|
||||
this._register(this.coreMouseService.onProtocolChange(events => {
|
||||
// apply global changes on events
|
||||
if (events) {
|
||||
if (this.optionsService.rawOptions.logLevel === 'debug') {
|
||||
this._logService.debug('Binding to mouse events:', this.coreMouseService.explainEvents(events));
|
||||
}
|
||||
this.element!.classList.add('enable-mouse-events');
|
||||
this._selectionService!.disable();
|
||||
} else {
|
||||
this._logService.debug('Unbinding from mouse events.');
|
||||
this.element!.classList.remove('enable-mouse-events');
|
||||
this._selectionService!.enable();
|
||||
}
|
||||
|
||||
// add/remove handlers from requestedEvents
|
||||
|
||||
if (!(events & CoreMouseEventType.MOVE)) {
|
||||
el.removeEventListener('mousemove', requestedEvents.mousemove!);
|
||||
requestedEvents.mousemove = null;
|
||||
} else if (!requestedEvents.mousemove) {
|
||||
el.addEventListener('mousemove', eventListeners.mousemove);
|
||||
requestedEvents.mousemove = eventListeners.mousemove;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.WHEEL)) {
|
||||
el.removeEventListener('wheel', requestedEvents.wheel!);
|
||||
requestedEvents.wheel = null;
|
||||
} else if (!requestedEvents.wheel) {
|
||||
el.addEventListener('wheel', eventListeners.wheel, { passive: false });
|
||||
requestedEvents.wheel = eventListeners.wheel;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.UP)) {
|
||||
this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
|
||||
requestedEvents.mouseup = null;
|
||||
} else {
|
||||
requestedEvents.mouseup ??= eventListeners.mouseup;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.DRAG)) {
|
||||
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!);
|
||||
requestedEvents.mousedrag = null;
|
||||
} else {
|
||||
requestedEvents.mousedrag ??= eventListeners.mousedrag;
|
||||
}
|
||||
}));
|
||||
// force initial onProtocolChange so we dont miss early mouse requests
|
||||
this.coreMouseService.activeProtocol = this.coreMouseService.activeProtocol;
|
||||
|
||||
// Ensure document-level listeners are removed on dispose
|
||||
this._register(toDisposable(() => {
|
||||
if (requestedEvents.mouseup) {
|
||||
this._document!.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
if (requestedEvents.mousedrag) {
|
||||
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* "Always on" event listeners.
|
||||
*/
|
||||
this._register(addDisposableListener(el, 'mousedown', (ev: MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
this.focus();
|
||||
|
||||
// Don't send the mouse button to the pty if mouse events are disabled or
|
||||
// if the selection manager is having selection forced (ie. a modifier is
|
||||
// held).
|
||||
if (!this.coreMouseService.areMouseEventsActive || this._selectionService!.shouldForceSelection(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendEvent(ev);
|
||||
|
||||
// Register additional global handlers which should keep reporting outside
|
||||
// of the terminal element.
|
||||
// Note: Other emulators also do this for 'mousedown' while a button
|
||||
// is held, we currently limit 'mousedown' to the terminal only.
|
||||
if (requestedEvents.mouseup) {
|
||||
this._document!.addEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
if (requestedEvents.mousedrag) {
|
||||
this._document!.addEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(el, 'wheel', (ev: WheelEvent) => {
|
||||
// do nothing, if app side handles wheel itself
|
||||
if (requestedEvents.wheel) return;
|
||||
|
||||
if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.buffer.hasScrollback) {
|
||||
// Convert wheel events into up/down events when the buffer does not have scrollback, this
|
||||
// enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
|
||||
// events are not enabled.
|
||||
// This used implementation used get the actual lines/partial lines scrolled from the
|
||||
// viewport but since moving to the new viewport implementation has been simplified to
|
||||
// simply send a single up or down sequence.
|
||||
|
||||
// Do nothing if there's no vertical scroll
|
||||
const deltaY = (ev as WheelEvent).deltaY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = self.coreMouseService.consumeWheelEvent(
|
||||
ev as WheelEvent,
|
||||
self._renderService?.dimensions?.device?.cell?.height,
|
||||
self._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Construct and send sequences
|
||||
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
||||
this.coreService.triggerDataEvent(sequence, true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}, { passive: false }));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
|
||||
* opportunity.
|
||||
@@ -991,6 +728,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void {
|
||||
this._customWheelEventHandler = customWheelEventHandler;
|
||||
this._mouseService?.setCustomWheelEventHandler(customWheelEventHandler);
|
||||
}
|
||||
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifie
|
||||
import { Disposable, dispose, toDisposable } from 'common/Lifecycle';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
|
||||
import { ILinkProviderService, IMouseCoordsService, IRenderService } from './services/Services';
|
||||
import { Emitter } from 'common/Event';
|
||||
import { addDisposableListener } from 'browser/Dom';
|
||||
|
||||
@@ -30,7 +30,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
|
||||
constructor(
|
||||
private readonly _element: HTMLElement,
|
||||
@IMouseService private readonly _mouseService: IMouseService,
|
||||
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ILinkProviderService private readonly _linkProviderService: ILinkProviderService
|
||||
@@ -60,7 +60,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
private _handleMouseMove(event: MouseEvent): void {
|
||||
this._lastMouseEvent = event;
|
||||
|
||||
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
|
||||
const position = this._positionFromMouseEvent(event, this._element);
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
@@ -222,7 +222,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
|
||||
const position = this._positionFromMouseEvent(event, this._element);
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService);
|
||||
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element);
|
||||
|
||||
if (!position) {
|
||||
return;
|
||||
@@ -312,7 +312,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
this._clearCurrentLink(start, end);
|
||||
if (this._lastMouseEvent) {
|
||||
// re-eval previously active link after changes
|
||||
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
|
||||
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element);
|
||||
if (position) {
|
||||
this._askForLink(position, false);
|
||||
}
|
||||
@@ -378,8 +378,8 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
* Get the buffer position from a mouse event
|
||||
* @param event
|
||||
*/
|
||||
private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement, mouseService: IMouseService): IBufferCellPosition | undefined {
|
||||
const coords = mouseService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows);
|
||||
private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement): IBufferCellPosition | undefined {
|
||||
const coords = this._mouseCoordsService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows);
|
||||
if (!coords) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -392,6 +392,14 @@ export class MockMouseService implements IMouseService {
|
||||
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public setCustomWheelEventHandler(): void {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public bindMouse(): void {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRenderService implements IRenderService {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { getWindow } from 'browser/Dom';
|
||||
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { ICharSizeService, IMouseCoordsService, IRenderService } from 'browser/services/Services';
|
||||
|
||||
export class MouseCoordsService implements IMouseCoordsService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IRenderService private readonly _renderService: IRenderService
|
||||
) {
|
||||
}
|
||||
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
|
||||
return getCoords(
|
||||
window,
|
||||
event,
|
||||
element,
|
||||
colCount,
|
||||
rowCount,
|
||||
this._charSizeService.hasValidSize,
|
||||
this._renderService.dimensions.css.cell.width,
|
||||
this._renderService.dimensions.css.cell.height,
|
||||
isSelection
|
||||
);
|
||||
}
|
||||
|
||||
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
|
||||
const coords = getCoordsRelativeToElement(getWindow(element), event, element);
|
||||
if (!this._charSizeService.hasValidSize) {
|
||||
return undefined;
|
||||
}
|
||||
coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1);
|
||||
coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1);
|
||||
return {
|
||||
col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
|
||||
row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
|
||||
x: Math.floor(coords[0]),
|
||||
y: Math.floor(coords[1])
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,45 +3,324 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { getWindow } from 'browser/Dom';
|
||||
import { ICharSizeService, IRenderService, IMouseService } from './Services';
|
||||
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { addDisposableListener } from 'browser/Dom';
|
||||
import { IBufferService, ICoreMouseService, ICoreService, ILogService, IOptionsService } from 'common/services/Services';
|
||||
import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, IDisposable } from 'common/Types';
|
||||
import { C0 } from 'common/data/EscapeSequences';
|
||||
import { toDisposable } from 'common/Lifecycle';
|
||||
import { CustomWheelEventHandler } from 'browser/Types';
|
||||
import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services';
|
||||
|
||||
type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>;
|
||||
|
||||
interface IMouseBindContext {
|
||||
readonly target: IMouseServiceTarget;
|
||||
readonly focus: () => void;
|
||||
readonly requestedEvents: RequestedMouseEvents;
|
||||
}
|
||||
|
||||
export class MouseService implements IMouseService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private _customWheelEventHandler: CustomWheelEventHandler | undefined;
|
||||
|
||||
constructor(
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService
|
||||
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
|
||||
@ICoreMouseService private readonly _coreMouseService: ICoreMouseService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ISelectionService private readonly _selectionService: ISelectionService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
}
|
||||
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
|
||||
return getCoords(
|
||||
window,
|
||||
event,
|
||||
element,
|
||||
colCount,
|
||||
rowCount,
|
||||
this._charSizeService.hasValidSize,
|
||||
this._renderService.dimensions.css.cell.width,
|
||||
this._renderService.dimensions.css.cell.height,
|
||||
isSelection
|
||||
);
|
||||
public bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void {
|
||||
const { element, document } = target;
|
||||
|
||||
/**
|
||||
* Event listener state handling.
|
||||
* We listen to the onProtocolChange event of CoreMouseService and put
|
||||
* requested listeners in `requestedEvents`. With this the listeners
|
||||
* have all bits to do the event listener juggling.
|
||||
* Note: 'mousedown' currently is "always on" and not managed
|
||||
* by onProtocolChange.
|
||||
*/
|
||||
const requestedEvents: RequestedMouseEvents = {
|
||||
mouseup: null,
|
||||
wheel: null,
|
||||
mousedrag: null,
|
||||
mousemove: null
|
||||
};
|
||||
const ctx: IMouseBindContext = { target, focus, requestedEvents };
|
||||
const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
|
||||
mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
|
||||
wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
|
||||
mousedrag: (ev: Event) => this._handleMouseDrag(ctx, ev as MouseEvent),
|
||||
mousemove: (ev: Event) => this._handleMouseMove(ctx, ev as MouseEvent)
|
||||
};
|
||||
register(this._coreMouseService.onProtocolChange(events => {
|
||||
this._handleProtocolChange(ctx, eventListeners, events);
|
||||
}));
|
||||
// force initial onProtocolChange so we dont miss early mouse requests
|
||||
this._coreMouseService.activeProtocol = this._coreMouseService.activeProtocol;
|
||||
|
||||
// Ensure document-level listeners are removed on dispose
|
||||
register(toDisposable(() => {
|
||||
if (requestedEvents.mouseup) {
|
||||
document.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
if (requestedEvents.mousedrag) {
|
||||
document.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* "Always on" event listeners.
|
||||
*/
|
||||
register(addDisposableListener(element, 'mousedown', (ev: MouseEvent) => this._handleMouseDown(ctx, ev)));
|
||||
register(addDisposableListener(element, 'wheel', (ev: WheelEvent) => this._handlePassiveWheel(ctx, ev), { passive: false }));
|
||||
}
|
||||
|
||||
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
|
||||
const coords = getCoordsRelativeToElement(getWindow(element), event, element);
|
||||
if (!this._charSizeService.hasValidSize) {
|
||||
return undefined;
|
||||
private _sendEvent(ctx: IMouseBindContext, ev: MouseEvent | WheelEvent): boolean {
|
||||
// Get mouse coordinates
|
||||
const pos = this._mouseCoordsService.getMouseReportCoords(ev as MouseEvent, ctx.target.screenElement);
|
||||
if (!pos) {
|
||||
return false;
|
||||
}
|
||||
coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1);
|
||||
coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1);
|
||||
return {
|
||||
col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
|
||||
row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
|
||||
x: Math.floor(coords[0]),
|
||||
y: Math.floor(coords[1])
|
||||
};
|
||||
|
||||
let but: CoreMouseButton;
|
||||
let action: CoreMouseAction | undefined;
|
||||
switch ((ev as MouseEvent & { overrideType?: string }).overrideType || ev.type) {
|
||||
case 'mousemove':
|
||||
action = CoreMouseAction.MOVE;
|
||||
if (ev.buttons === undefined) {
|
||||
// buttons is not supported on macOS, try to get a value from button instead
|
||||
but = CoreMouseButton.NONE;
|
||||
if (ev.button !== undefined) {
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
}
|
||||
} else {
|
||||
// according to MDN buttons only reports up to button 5 (AUX2)
|
||||
but = ev.buttons & 1 ? CoreMouseButton.LEFT :
|
||||
ev.buttons & 4 ? CoreMouseButton.MIDDLE :
|
||||
ev.buttons & 2 ? CoreMouseButton.RIGHT :
|
||||
CoreMouseButton.NONE; // fallback to NONE
|
||||
}
|
||||
break;
|
||||
case 'mouseup':
|
||||
action = CoreMouseAction.UP;
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'mousedown':
|
||||
action = CoreMouseAction.DOWN;
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'wheel':
|
||||
if (this._customWheelEventHandler && this._customWheelEventHandler(ev as WheelEvent) === false) {
|
||||
return false;
|
||||
}
|
||||
const deltaY = (ev as WheelEvent).deltaY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
const lines = this._coreMouseService.consumeWheelEvent(
|
||||
ev as WheelEvent,
|
||||
this._renderService?.dimensions?.device?.cell?.height,
|
||||
this._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
return false;
|
||||
}
|
||||
action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
|
||||
but = CoreMouseButton.WHEEL;
|
||||
break;
|
||||
default:
|
||||
// dont handle other event types by accident
|
||||
return false;
|
||||
}
|
||||
|
||||
// exit if we cannot determine valid button/action values
|
||||
// do nothing for higher buttons than wheel
|
||||
if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._coreMouseService.triggerMouseEvent({
|
||||
col: pos.col,
|
||||
row: pos.row,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
button: but,
|
||||
action,
|
||||
ctrl: ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
shift: ev.shiftKey
|
||||
});
|
||||
}
|
||||
|
||||
private _handleMouseUp(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
this._sendEvent(ctx, ev);
|
||||
if (!ev.buttons) {
|
||||
// if no other button is held remove global handlers
|
||||
if (ctx.requestedEvents.mouseup) {
|
||||
ctx.target.document.removeEventListener('mouseup', ctx.requestedEvents.mouseup);
|
||||
}
|
||||
if (ctx.requestedEvents.mousedrag) {
|
||||
ctx.target.document.removeEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleWheel(ctx: IMouseBindContext, ev: WheelEvent): false {
|
||||
this._sendEvent(ctx, ev);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
private _handleMouseDrag(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
// deal only with move while a button is held
|
||||
if (ev.buttons) {
|
||||
this._sendEvent(ctx, ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMouseMove(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
// deal only with move without any button
|
||||
if (!ev.buttons) {
|
||||
this._sendEvent(ctx, ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMouseDown(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
ev.preventDefault();
|
||||
ctx.focus();
|
||||
|
||||
// Don't send the mouse button to the pty if mouse events are disabled or
|
||||
// if the selection manager is having selection forced (ie. a modifier is
|
||||
// held).
|
||||
if (!this._coreMouseService.areMouseEventsActive || this._selectionService.shouldForceSelection(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._sendEvent(ctx, ev);
|
||||
|
||||
// Register additional global handlers which should keep reporting outside
|
||||
// of the terminal element.
|
||||
// Note: Other emulators also do this for 'mousedown' while a button
|
||||
// is held, we currently limit 'mousedown' to the terminal only.
|
||||
if (ctx.requestedEvents.mouseup) {
|
||||
ctx.target.document.addEventListener('mouseup', ctx.requestedEvents.mouseup);
|
||||
}
|
||||
if (ctx.requestedEvents.mousedrag) {
|
||||
ctx.target.document.addEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePassiveWheel(ctx: IMouseBindContext, ev: WheelEvent): false | void {
|
||||
// do nothing, if app side handles wheel itself
|
||||
if (ctx.requestedEvents.wheel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._bufferService.buffer.hasScrollback) {
|
||||
// Convert wheel events into up/down events when the buffer does not have scrollback, this
|
||||
// enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
|
||||
// events are not enabled.
|
||||
// This used implementation used get the actual lines/partial lines scrolled from the
|
||||
// viewport but since moving to the new viewport implementation has been simplified to
|
||||
// simply send a single up or down sequence.
|
||||
|
||||
// Do nothing if there's no vertical scroll
|
||||
const deltaY = ev.deltaY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = this._coreMouseService.consumeWheelEvent(
|
||||
ev,
|
||||
this._renderService?.dimensions?.device?.cell?.height,
|
||||
this._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Construct and send sequences
|
||||
const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
||||
this._coreService.triggerDataEvent(sequence, true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
|
||||
const { element, document } = ctx.target;
|
||||
const { requestedEvents } = ctx;
|
||||
// apply global changes on events
|
||||
if (events) {
|
||||
if (this._optionsService.rawOptions.logLevel === 'debug') {
|
||||
this._logService.debug('Binding to mouse events:', this._coreMouseService.explainEvents(events));
|
||||
}
|
||||
element.classList.add('enable-mouse-events');
|
||||
this._selectionService.disable();
|
||||
} else {
|
||||
this._logService.debug('Unbinding from mouse events.');
|
||||
element.classList.remove('enable-mouse-events');
|
||||
this._selectionService.enable();
|
||||
}
|
||||
|
||||
// add/remove handlers from requestedEvents
|
||||
if (!(events & CoreMouseEventType.MOVE)) {
|
||||
if (requestedEvents.mousemove) {
|
||||
element.removeEventListener('mousemove', requestedEvents.mousemove);
|
||||
}
|
||||
requestedEvents.mousemove = null;
|
||||
} else if (!requestedEvents.mousemove) {
|
||||
element.addEventListener('mousemove', eventListeners.mousemove);
|
||||
requestedEvents.mousemove = eventListeners.mousemove;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.WHEEL)) {
|
||||
if (requestedEvents.wheel) {
|
||||
element.removeEventListener('wheel', requestedEvents.wheel);
|
||||
}
|
||||
requestedEvents.wheel = null;
|
||||
} else if (!requestedEvents.wheel) {
|
||||
element.addEventListener('wheel', eventListeners.wheel, { passive: false });
|
||||
requestedEvents.wheel = eventListeners.wheel;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.UP)) {
|
||||
if (requestedEvents.mouseup) {
|
||||
document.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
requestedEvents.mouseup = null;
|
||||
} else {
|
||||
requestedEvents.mouseup ??= eventListeners.mouseup;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.DRAG)) {
|
||||
if (requestedEvents.mousedrag) {
|
||||
document.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
requestedEvents.mousedrag = null;
|
||||
} else {
|
||||
requestedEvents.mousedrag ??= eventListeners.mousedrag;
|
||||
}
|
||||
}
|
||||
|
||||
public setCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler | undefined): void {
|
||||
this._customWheelEventHandler = customWheelEventHandler;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { moveToCellSequence } from 'browser/input/MoveToCell';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { ICoreBrowserService, IMouseCoordsService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { IBufferLine, ICellData, IDisposable } from 'common/Types';
|
||||
@@ -126,7 +126,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
private readonly _linkifier: ILinkifier2,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IMouseService private readonly _mouseService: IMouseService,
|
||||
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
@@ -395,7 +395,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseBufferCoords(event: MouseEvent): [number, number] | undefined {
|
||||
const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
|
||||
const coords = this._mouseCoordsService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
|
||||
if (!coords) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -715,7 +715,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
|
||||
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.rawOptions.altClickMovesCursor) {
|
||||
if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) {
|
||||
const coordinates = this._mouseService.getCoords(
|
||||
const coordinates = this._mouseCoordsService.getCoords(
|
||||
event,
|
||||
this._element,
|
||||
this._bufferService.cols,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
|
||||
import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types';
|
||||
import { CustomWheelEventHandler, IColorSet, ILink, ReadonlyColorSet } from 'browser/Types';
|
||||
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
import { AllColorIndex, IDisposable, IKeyboardResult } from 'common/Types';
|
||||
@@ -49,14 +49,27 @@ export interface ICoreBrowserService {
|
||||
readonly dpr: number;
|
||||
}
|
||||
|
||||
export const IMouseService = createDecorator<IMouseService>('MouseService');
|
||||
export interface IMouseService {
|
||||
export const IMouseCoordsService = createDecorator<IMouseCoordsService>('MouseCoordsService');
|
||||
export interface IMouseCoordsService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined;
|
||||
getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined;
|
||||
}
|
||||
|
||||
export const IMouseService = createDecorator<IMouseService>('MouseService');
|
||||
export interface IMouseService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
setCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler | undefined): void;
|
||||
bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void;
|
||||
}
|
||||
export interface IMouseServiceTarget {
|
||||
element: HTMLElement;
|
||||
screenElement: HTMLElement;
|
||||
document: Document;
|
||||
}
|
||||
|
||||
export const IRenderService = createDecorator<IRenderService>('RenderService');
|
||||
export interface IRenderService extends IDisposable {
|
||||
serviceBrand: undefined;
|
||||
|
||||
Reference in New Issue
Block a user