@xterm/xterm 6.1.0-beta.183 → 6.1.0-beta.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,45 +3,324 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
- import { getWindow } from 'browser/Dom';
7
- import { ICharSizeService, IRenderService, IMouseService } from './Services';
8
- import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
6
+ import { addDisposableListener } from 'browser/Dom';
7
+ import { IBufferService, ICoreMouseService, ICoreService, ILogService, IOptionsService } from 'common/services/Services';
8
+ import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, IDisposable } from 'common/Types';
9
+ import { C0 } from 'common/data/EscapeSequences';
10
+ import { toDisposable } from 'common/Lifecycle';
11
+ import { CustomWheelEventHandler } from 'browser/Types';
12
+ import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services';
13
+
14
+ type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>;
15
+
16
+ interface IMouseBindContext {
17
+ readonly target: IMouseServiceTarget;
18
+ readonly focus: () => void;
19
+ readonly requestedEvents: RequestedMouseEvents;
20
+ }
9
21
 
10
22
  export class MouseService implements IMouseService {
11
23
  public serviceBrand: undefined;
12
24
 
25
+ private _customWheelEventHandler: CustomWheelEventHandler | undefined;
26
+
13
27
  constructor(
14
28
  @IRenderService private readonly _renderService: IRenderService,
15
- @ICharSizeService private readonly _charSizeService: ICharSizeService
29
+ @IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
30
+ @ICoreMouseService private readonly _coreMouseService: ICoreMouseService,
31
+ @ICoreService private readonly _coreService: ICoreService,
32
+ @IBufferService private readonly _bufferService: IBufferService,
33
+ @IOptionsService private readonly _optionsService: IOptionsService,
34
+ @ISelectionService private readonly _selectionService: ISelectionService,
35
+ @ILogService private readonly _logService: ILogService,
36
+ @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
16
37
  ) {
17
38
  }
18
39
 
19
- public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
20
- return getCoords(
21
- window,
22
- event,
23
- element,
24
- colCount,
25
- rowCount,
26
- this._charSizeService.hasValidSize,
27
- this._renderService.dimensions.css.cell.width,
28
- this._renderService.dimensions.css.cell.height,
29
- isSelection
30
- );
31
- }
32
-
33
- public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
34
- const coords = getCoordsRelativeToElement(getWindow(element), event, element);
35
- if (!this._charSizeService.hasValidSize) {
36
- return undefined;
37
- }
38
- coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1);
39
- coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1);
40
- return {
41
- col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
42
- row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
43
- x: Math.floor(coords[0]),
44
- y: Math.floor(coords[1])
40
+ public bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void {
41
+ const { element, document } = target;
42
+
43
+ /**
44
+ * Event listener state handling.
45
+ * We listen to the onProtocolChange event of CoreMouseService and put
46
+ * requested listeners in `requestedEvents`. With this the listeners
47
+ * have all bits to do the event listener juggling.
48
+ * Note: 'mousedown' currently is "always on" and not managed
49
+ * by onProtocolChange.
50
+ */
51
+ const requestedEvents: RequestedMouseEvents = {
52
+ mouseup: null,
53
+ wheel: null,
54
+ mousedrag: null,
55
+ mousemove: null
56
+ };
57
+ const ctx: IMouseBindContext = { target, focus, requestedEvents };
58
+ const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
59
+ mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
60
+ wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
61
+ mousedrag: (ev: Event) => this._handleMouseDrag(ctx, ev as MouseEvent),
62
+ mousemove: (ev: Event) => this._handleMouseMove(ctx, ev as MouseEvent)
45
63
  };
64
+ register(this._coreMouseService.onProtocolChange(events => {
65
+ this._handleProtocolChange(ctx, eventListeners, events);
66
+ }));
67
+ // force initial onProtocolChange so we dont miss early mouse requests
68
+ this._coreMouseService.activeProtocol = this._coreMouseService.activeProtocol;
69
+
70
+ // Ensure document-level listeners are removed on dispose
71
+ register(toDisposable(() => {
72
+ if (requestedEvents.mouseup) {
73
+ document.removeEventListener('mouseup', requestedEvents.mouseup);
74
+ }
75
+ if (requestedEvents.mousedrag) {
76
+ document.removeEventListener('mousemove', requestedEvents.mousedrag);
77
+ }
78
+ }));
79
+
80
+ /**
81
+ * "Always on" event listeners.
82
+ */
83
+ register(addDisposableListener(element, 'mousedown', (ev: MouseEvent) => this._handleMouseDown(ctx, ev)));
84
+ register(addDisposableListener(element, 'wheel', (ev: WheelEvent) => this._handlePassiveWheel(ctx, ev), { passive: false }));
85
+ }
86
+
87
+ private _sendEvent(ctx: IMouseBindContext, ev: MouseEvent | WheelEvent): boolean {
88
+ // Get mouse coordinates
89
+ const pos = this._mouseCoordsService.getMouseReportCoords(ev as MouseEvent, ctx.target.screenElement);
90
+ if (!pos) {
91
+ return false;
92
+ }
93
+
94
+ let but: CoreMouseButton;
95
+ let action: CoreMouseAction | undefined;
96
+ switch ((ev as MouseEvent & { overrideType?: string }).overrideType || ev.type) {
97
+ case 'mousemove':
98
+ action = CoreMouseAction.MOVE;
99
+ if (ev.buttons === undefined) {
100
+ // buttons is not supported on macOS, try to get a value from button instead
101
+ but = CoreMouseButton.NONE;
102
+ if (ev.button !== undefined) {
103
+ but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
104
+ }
105
+ } else {
106
+ // according to MDN buttons only reports up to button 5 (AUX2)
107
+ but = ev.buttons & 1 ? CoreMouseButton.LEFT :
108
+ ev.buttons & 4 ? CoreMouseButton.MIDDLE :
109
+ ev.buttons & 2 ? CoreMouseButton.RIGHT :
110
+ CoreMouseButton.NONE; // fallback to NONE
111
+ }
112
+ break;
113
+ case 'mouseup':
114
+ action = CoreMouseAction.UP;
115
+ but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
116
+ break;
117
+ case 'mousedown':
118
+ action = CoreMouseAction.DOWN;
119
+ but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
120
+ break;
121
+ case 'wheel':
122
+ if (this._customWheelEventHandler && this._customWheelEventHandler(ev as WheelEvent) === false) {
123
+ return false;
124
+ }
125
+ const deltaY = (ev as WheelEvent).deltaY;
126
+ if (deltaY === 0) {
127
+ return false;
128
+ }
129
+ const lines = this._coreMouseService.consumeWheelEvent(
130
+ ev as WheelEvent,
131
+ this._renderService?.dimensions?.device?.cell?.height,
132
+ this._coreBrowserService?.dpr
133
+ );
134
+ if (lines === 0) {
135
+ return false;
136
+ }
137
+ action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
138
+ but = CoreMouseButton.WHEEL;
139
+ break;
140
+ default:
141
+ // dont handle other event types by accident
142
+ return false;
143
+ }
144
+
145
+ // exit if we cannot determine valid button/action values
146
+ // do nothing for higher buttons than wheel
147
+ if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
148
+ return false;
149
+ }
150
+
151
+ return this._coreMouseService.triggerMouseEvent({
152
+ col: pos.col,
153
+ row: pos.row,
154
+ x: pos.x,
155
+ y: pos.y,
156
+ button: but,
157
+ action,
158
+ ctrl: ev.ctrlKey,
159
+ alt: ev.altKey,
160
+ shift: ev.shiftKey
161
+ });
162
+ }
163
+
164
+ private _handleMouseUp(ctx: IMouseBindContext, ev: MouseEvent): void {
165
+ this._sendEvent(ctx, ev);
166
+ if (!ev.buttons) {
167
+ // if no other button is held remove global handlers
168
+ if (ctx.requestedEvents.mouseup) {
169
+ ctx.target.document.removeEventListener('mouseup', ctx.requestedEvents.mouseup);
170
+ }
171
+ if (ctx.requestedEvents.mousedrag) {
172
+ ctx.target.document.removeEventListener('mousemove', ctx.requestedEvents.mousedrag);
173
+ }
174
+ }
175
+ }
176
+
177
+ private _handleWheel(ctx: IMouseBindContext, ev: WheelEvent): false {
178
+ this._sendEvent(ctx, ev);
179
+ ev.preventDefault();
180
+ ev.stopPropagation();
181
+ return false;
182
+ }
183
+
184
+ private _handleMouseDrag(ctx: IMouseBindContext, ev: MouseEvent): void {
185
+ // deal only with move while a button is held
186
+ if (ev.buttons) {
187
+ this._sendEvent(ctx, ev);
188
+ }
189
+ }
190
+
191
+ private _handleMouseMove(ctx: IMouseBindContext, ev: MouseEvent): void {
192
+ // deal only with move without any button
193
+ if (!ev.buttons) {
194
+ this._sendEvent(ctx, ev);
195
+ }
196
+ }
197
+
198
+ private _handleMouseDown(ctx: IMouseBindContext, ev: MouseEvent): void {
199
+ ev.preventDefault();
200
+ ctx.focus();
201
+
202
+ // Don't send the mouse button to the pty if mouse events are disabled or
203
+ // if the selection manager is having selection forced (ie. a modifier is
204
+ // held).
205
+ if (!this._coreMouseService.areMouseEventsActive || this._selectionService.shouldForceSelection(ev)) {
206
+ return;
207
+ }
208
+
209
+ this._sendEvent(ctx, ev);
210
+
211
+ // Register additional global handlers which should keep reporting outside
212
+ // of the terminal element.
213
+ // Note: Other emulators also do this for 'mousedown' while a button
214
+ // is held, we currently limit 'mousedown' to the terminal only.
215
+ if (ctx.requestedEvents.mouseup) {
216
+ ctx.target.document.addEventListener('mouseup', ctx.requestedEvents.mouseup);
217
+ }
218
+ if (ctx.requestedEvents.mousedrag) {
219
+ ctx.target.document.addEventListener('mousemove', ctx.requestedEvents.mousedrag);
220
+ }
221
+ }
222
+
223
+ private _handlePassiveWheel(ctx: IMouseBindContext, ev: WheelEvent): false | void {
224
+ // do nothing, if app side handles wheel itself
225
+ if (ctx.requestedEvents.wheel) {
226
+ return;
227
+ }
228
+
229
+ if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
230
+ return false;
231
+ }
232
+
233
+ if (!this._bufferService.buffer.hasScrollback) {
234
+ // Convert wheel events into up/down events when the buffer does not have scrollback, this
235
+ // enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
236
+ // events are not enabled.
237
+ // This used implementation used get the actual lines/partial lines scrolled from the
238
+ // viewport but since moving to the new viewport implementation has been simplified to
239
+ // simply send a single up or down sequence.
240
+
241
+ // Do nothing if there's no vertical scroll
242
+ const deltaY = ev.deltaY;
243
+ if (deltaY === 0) {
244
+ return false;
245
+ }
246
+
247
+ const lines = this._coreMouseService.consumeWheelEvent(
248
+ ev,
249
+ this._renderService?.dimensions?.device?.cell?.height,
250
+ this._coreBrowserService?.dpr
251
+ );
252
+ if (lines === 0) {
253
+ ev.preventDefault();
254
+ ev.stopPropagation();
255
+ return false;
256
+ }
257
+
258
+ // Construct and send sequences
259
+ const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
260
+ this._coreService.triggerDataEvent(sequence, true);
261
+ ev.preventDefault();
262
+ ev.stopPropagation();
263
+ return false;
264
+ }
265
+ }
266
+
267
+ private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
268
+ const { element, document } = ctx.target;
269
+ const { requestedEvents } = ctx;
270
+ // apply global changes on events
271
+ if (events) {
272
+ if (this._optionsService.rawOptions.logLevel === 'debug') {
273
+ this._logService.debug('Binding to mouse events:', this._coreMouseService.explainEvents(events));
274
+ }
275
+ element.classList.add('enable-mouse-events');
276
+ this._selectionService.disable();
277
+ } else {
278
+ this._logService.debug('Unbinding from mouse events.');
279
+ element.classList.remove('enable-mouse-events');
280
+ this._selectionService.enable();
281
+ }
282
+
283
+ // add/remove handlers from requestedEvents
284
+ if (!(events & CoreMouseEventType.MOVE)) {
285
+ if (requestedEvents.mousemove) {
286
+ element.removeEventListener('mousemove', requestedEvents.mousemove);
287
+ }
288
+ requestedEvents.mousemove = null;
289
+ } else if (!requestedEvents.mousemove) {
290
+ element.addEventListener('mousemove', eventListeners.mousemove);
291
+ requestedEvents.mousemove = eventListeners.mousemove;
292
+ }
293
+
294
+ if (!(events & CoreMouseEventType.WHEEL)) {
295
+ if (requestedEvents.wheel) {
296
+ element.removeEventListener('wheel', requestedEvents.wheel);
297
+ }
298
+ requestedEvents.wheel = null;
299
+ } else if (!requestedEvents.wheel) {
300
+ element.addEventListener('wheel', eventListeners.wheel, { passive: false });
301
+ requestedEvents.wheel = eventListeners.wheel;
302
+ }
303
+
304
+ if (!(events & CoreMouseEventType.UP)) {
305
+ if (requestedEvents.mouseup) {
306
+ document.removeEventListener('mouseup', requestedEvents.mouseup);
307
+ }
308
+ requestedEvents.mouseup = null;
309
+ } else {
310
+ requestedEvents.mouseup ??= eventListeners.mouseup;
311
+ }
312
+
313
+ if (!(events & CoreMouseEventType.DRAG)) {
314
+ if (requestedEvents.mousedrag) {
315
+ document.removeEventListener('mousemove', requestedEvents.mousedrag);
316
+ }
317
+ requestedEvents.mousedrag = null;
318
+ } else {
319
+ requestedEvents.mousedrag ??= eventListeners.mousedrag;
320
+ }
321
+ }
322
+
323
+ public setCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler | undefined): void {
324
+ this._customWheelEventHandler = customWheelEventHandler;
46
325
  }
47
326
  }
@@ -8,7 +8,7 @@ import { getCoordsRelativeToElement } from 'browser/input/Mouse';
8
8
  import { moveToCellSequence } from 'browser/input/MoveToCell';
9
9
  import { SelectionModel } from 'browser/selection/SelectionModel';
10
10
  import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
11
- import { ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
11
+ import { ICoreBrowserService, IMouseCoordsService, IRenderService, ISelectionService } from 'browser/services/Services';
12
12
  import { Disposable, toDisposable } from 'common/Lifecycle';
13
13
  import * as Browser from 'common/Platform';
14
14
  import { IBufferLine, ICellData, IDisposable } from 'common/Types';
@@ -126,7 +126,7 @@ export class SelectionService extends Disposable implements ISelectionService {
126
126
  private readonly _linkifier: ILinkifier2,
127
127
  @IBufferService private readonly _bufferService: IBufferService,
128
128
  @ICoreService private readonly _coreService: ICoreService,
129
- @IMouseService private readonly _mouseService: IMouseService,
129
+ @IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
130
130
  @IOptionsService private readonly _optionsService: IOptionsService,
131
131
  @IRenderService private readonly _renderService: IRenderService,
132
132
  @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
@@ -395,7 +395,7 @@ export class SelectionService extends Disposable implements ISelectionService {
395
395
  * @param event The mouse event.
396
396
  */
397
397
  private _getMouseBufferCoords(event: MouseEvent): [number, number] | undefined {
398
- const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
398
+ const coords = this._mouseCoordsService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
399
399
  if (!coords) {
400
400
  return undefined;
401
401
  }
@@ -715,7 +715,7 @@ export class SelectionService extends Disposable implements ISelectionService {
715
715
 
716
716
  if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.rawOptions.altClickMovesCursor) {
717
717
  if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) {
718
- const coordinates = this._mouseService.getCoords(
718
+ const coordinates = this._mouseCoordsService.getCoords(
719
719
  event,
720
720
  this._element,
721
721
  this._bufferService.cols,
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
7
- import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types';
7
+ import { CustomWheelEventHandler, IColorSet, ILink, ReadonlyColorSet } from 'browser/Types';
8
8
  import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
9
9
  import { createDecorator } from 'common/services/ServiceRegistry';
10
10
  import { AllColorIndex, IDisposable, IKeyboardResult } from 'common/Types';
@@ -49,14 +49,27 @@ export interface ICoreBrowserService {
49
49
  readonly dpr: number;
50
50
  }
51
51
 
52
- export const IMouseService = createDecorator<IMouseService>('MouseService');
53
- export interface IMouseService {
52
+ export const IMouseCoordsService = createDecorator<IMouseCoordsService>('MouseCoordsService');
53
+ export interface IMouseCoordsService {
54
54
  serviceBrand: undefined;
55
55
 
56
56
  getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined;
57
57
  getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined;
58
58
  }
59
59
 
60
+ export const IMouseService = createDecorator<IMouseService>('MouseService');
61
+ export interface IMouseService {
62
+ serviceBrand: undefined;
63
+
64
+ setCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler | undefined): void;
65
+ bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void;
66
+ }
67
+ export interface IMouseServiceTarget {
68
+ element: HTMLElement;
69
+ screenElement: HTMLElement;
70
+ document: Document;
71
+ }
72
+
60
73
  export const IRenderService = createDecorator<IRenderService>('RenderService');
61
74
  export interface IRenderService extends IDisposable {
62
75
  serviceBrand: undefined;
@@ -6,4 +6,4 @@
6
6
  /**
7
7
  * The xterm.js version. This is updated by the publish script from package.json.
8
8
  */
9
- export const XTERM_VERSION = '6.1.0-beta.183';
9
+ export const XTERM_VERSION = '6.1.0-beta.184';