Skip to content

Commit be772cc

Browse files
committed
Fix microsoft#83719. PointerEvent handler for iOS.
1 parent 5076757 commit be772cc

6 files changed

Lines changed: 168 additions & 15 deletions

File tree

src/vs/base/browser/canIUse.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ export const BrowserFeatures = {
5555
return KeyboardSupport.None;
5656
})(),
5757

58-
touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0
58+
touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0,
59+
pointerEvents: browser.isSafari && ('ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0)
5960
};

src/vs/base/browser/dom.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,21 @@ export function addDisposableNonBubblingMouseOutListener(node: Element, handler:
281281
});
282282
}
283283

284+
export function addDisposableNonBubblingPointerOutListener(node: Element, handler: (event: MouseEvent) => void): IDisposable {
285+
return addDisposableListener(node, 'pointerout', (e: MouseEvent) => {
286+
// Mouse out bubbles, so this is an attempt to ignore faux mouse outs coming from children elements
287+
let toElement: Node | null = <Node>(e.relatedTarget || e.target);
288+
while (toElement && toElement !== node) {
289+
toElement = toElement.parentNode;
290+
}
291+
if (toElement === node) {
292+
return;
293+
}
294+
295+
handler(e);
296+
});
297+
}
298+
284299
interface IRequestAnimationFrame {
285300
(callback: (time: number) => void): number;
286301
}
@@ -852,6 +867,8 @@ export const EventType = {
852867
MOUSE_OUT: 'mouseout',
853868
MOUSE_ENTER: 'mouseenter',
854869
MOUSE_LEAVE: 'mouseleave',
870+
POINTER_UP: 'pointerup',
871+
POINTER_DOWN: 'pointerdown',
855872
CONTEXT_MENU: 'contextmenu',
856873
WHEEL: 'wheel',
857874
// Keyboard

src/vs/base/browser/globalMouseMoveMonitor.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ export function standardMouseMoveMerger(lastEvent: IStandardMouseMoveEventData,
3838

3939
export class GlobalMouseMoveMonitor<R> implements IDisposable {
4040

41-
private readonly hooks = new DisposableStore();
42-
private mouseMoveEventMerger: IEventMerger<R> | null = null;
43-
private mouseMoveCallback: IMouseMoveCallback<R> | null = null;
44-
private onStopCallback: IOnStopCallback | null = null;
41+
protected readonly hooks = new DisposableStore();
42+
protected mouseMoveEventMerger: IEventMerger<R> | null = null;
43+
protected mouseMoveCallback: IMouseMoveCallback<R> | null = null;
44+
protected onStopCallback: IOnStopCallback | null = null;
4545

4646
public dispose(): void {
4747
this.stopMonitoring(false);
@@ -116,3 +116,32 @@ export class GlobalMouseMoveMonitor<R> implements IDisposable {
116116
}
117117
}
118118
}
119+
120+
export class GlobalPointerMoveMonitor<R> extends GlobalMouseMoveMonitor<R> {
121+
public startMonitoring(
122+
mouseMoveEventMerger: IEventMerger<R>,
123+
mouseMoveCallback: IMouseMoveCallback<R>,
124+
onStopCallback: IOnStopCallback
125+
): void {
126+
if (this.isMonitoring()) {
127+
// I am already hooked
128+
return;
129+
}
130+
this.mouseMoveEventMerger = mouseMoveEventMerger;
131+
this.mouseMoveCallback = mouseMoveCallback;
132+
this.onStopCallback = onStopCallback;
133+
134+
let windowChain = IframeUtils.getSameOriginWindowChain();
135+
for (const element of windowChain) {
136+
this.hooks.add(dom.addDisposableThrottledListener(element.window.document, 'pointermove',
137+
(data: R) => {
138+
this.mouseMoveCallback!(data);
139+
},
140+
(lastEvent: R, currentEvent) => this.mouseMoveEventMerger!(lastEvent, currentEvent as MouseEvent)
141+
));
142+
this.hooks.add(dom.addDisposableListener(element.window.document, 'pointerup', (e: MouseEvent) => this.stopMonitoring(true)));
143+
}
144+
145+
// Currently we didn't test pointer events in iframe yet.
146+
}
147+
}

src/vs/editor/browser/controller/mouseHandler.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions';
2626
/**
2727
* Merges mouse events when mouse move events are throttled
2828
*/
29-
function createMouseMoveEventMerger(mouseTargetFactory: MouseTargetFactory | null) {
29+
export function createMouseMoveEventMerger(mouseTargetFactory: MouseTargetFactory | null) {
3030
return function (lastEvent: EditorMouseEvent, currentEvent: EditorMouseEvent): EditorMouseEvent {
3131
let targetIsWidget = false;
3232
if (mouseTargetFactory) {
@@ -71,8 +71,7 @@ export class MouseHandler extends ViewEventHandler {
7171
protected viewHelper: IPointerHandlerHelper;
7272
protected mouseTargetFactory: MouseTargetFactory;
7373
private readonly _asyncFocus: RunOnceScheduler;
74-
75-
private readonly _mouseDownOperation: MouseDownOperation;
74+
protected readonly _mouseDownOperation: MouseDownOperation;
7675
private lastMouseLeaveTime: number;
7776

7877
constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) {
@@ -179,7 +178,7 @@ export class MouseHandler extends ViewEventHandler {
179178
});
180179
}
181180

182-
private _onMouseMove(e: EditorMouseEvent): void {
181+
public _onMouseMove(e: EditorMouseEvent): void {
183182
if (this._mouseDownOperation.isActive()) {
184183
// In selection/drag operation
185184
return;
@@ -196,7 +195,7 @@ export class MouseHandler extends ViewEventHandler {
196195
});
197196
}
198197

199-
private _onMouseLeave(e: EditorMouseEvent): void {
198+
public _onMouseLeave(e: EditorMouseEvent): void {
200199
this.lastMouseLeaveTime = (new Date()).getTime();
201200
this.viewController.emitMouseLeave({
202201
event: e,

src/vs/editor/browser/controller/pointerHandler.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import * as dom from 'vs/base/browser/dom';
77
import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
88
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
9-
import { IPointerHandlerHelper, MouseHandler } from 'vs/editor/browser/controller/mouseHandler';
9+
import { IPointerHandlerHelper, MouseHandler, createMouseMoveEventMerger } from 'vs/editor/browser/controller/mouseHandler';
1010
import { IMouseTarget } from 'vs/editor/browser/editorBrowser';
11-
import { EditorMouseEvent } from 'vs/editor/browser/editorDom';
11+
import { EditorMouseEvent, EditorPointerEventFactory } from 'vs/editor/browser/editorDom';
1212
import { ViewController } from 'vs/editor/browser/view/viewController';
1313
import { ViewContext } from 'vs/editor/common/view/viewContext';
14+
import { isSafari } from 'vs/base/browser/browser';
1415

1516
interface IThrottledGestureEvent {
1617
translationX: number;
@@ -185,6 +186,66 @@ class StandardPointerHandler extends MouseHandler implements IDisposable {
185186
}
186187
}
187188

189+
/**
190+
* Currently only tested on iOS 13/ iPadOS.
191+
*/
192+
export class PointerEventHandler extends MouseHandler {
193+
private _lastPointerType: string;
194+
constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) {
195+
super(context, viewController, viewHelper);
196+
197+
this._register(Gesture.addTarget(this.viewHelper.linesContentDomNode));
198+
this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Tap, (e) => this.onTap(e)));
199+
this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Change, (e) => this.onChange(e)));
200+
201+
this._lastPointerType = 'mouse';
202+
203+
this.viewHelper.linesContentDomNode.addEventListener('pointerdown', (e: any) => {
204+
const pointerType = <any>e.pointerType;
205+
if (pointerType === 'mouse') {
206+
this._lastPointerType = 'mouse';
207+
return;
208+
} else if (pointerType === 'touch') {
209+
this._lastPointerType = 'touch';
210+
} else {
211+
this._lastPointerType = 'pen';
212+
}
213+
});
214+
215+
// PonterEvents
216+
const pointerEvents = new EditorPointerEventFactory(this.viewHelper.viewDomNode);
217+
218+
this._register(pointerEvents.onPointerMoveThrottled(this.viewHelper.viewDomNode,
219+
(e) => this._onMouseMove(e),
220+
createMouseMoveEventMerger(this.mouseTargetFactory), MouseHandler.MOUSE_MOVE_MINIMUM_TIME));
221+
this._register(pointerEvents.onPointerUp(this.viewHelper.viewDomNode, (e) => this._onMouseUp(e)));
222+
this._register(pointerEvents.onPointerLeave(this.viewHelper.viewDomNode, (e) => this._onMouseLeave(e)));
223+
this._register(pointerEvents.onPointerDown(this.viewHelper.viewDomNode, (e) => this._onMouseDown(e)));
224+
}
225+
226+
private onTap(event: GestureEvent): void {
227+
event.preventDefault();
228+
this.viewHelper.focusTextArea();
229+
const target = this._createMouseTarget(new EditorMouseEvent(event, this.viewHelper.viewDomNode), false);
230+
231+
if (target.position) {
232+
this.viewController.moveTo(target.position);
233+
}
234+
}
235+
236+
private onChange(e: GestureEvent): void {
237+
if (this._lastPointerType === 'touch') {
238+
this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY);
239+
}
240+
}
241+
242+
public _onMouseDown(e: EditorMouseEvent): void {
243+
if (this._lastPointerType !== 'touch') {
244+
super._onMouseDown(e);
245+
}
246+
}
247+
}
248+
188249
class TouchHandler extends MouseHandler {
189250

190251
constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) {
@@ -221,6 +282,8 @@ export class PointerHandler extends Disposable {
221282
super();
222283
if (window.navigator.msPointerEnabled) {
223284
this.handler = this._register(new MsPointerHandler(context, viewController, viewHelper));
285+
} else if (((<any>window).PointerEvent && isSafari)) {
286+
this.handler = this._register(new PointerEventHandler(context, viewController, viewHelper));
224287
} else if ((<any>window).TouchEvent) {
225288
this.handler = this._register(new TouchHandler(context, viewController, viewHelper));
226289
} else if (window.navigator.pointerEnabled || (<any>window).PointerEvent) {

src/vs/editor/browser/editorDom.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as dom from 'vs/base/browser/dom';
7-
import { GlobalMouseMoveMonitor } from 'vs/base/browser/globalMouseMoveMonitor';
7+
import { GlobalMouseMoveMonitor, GlobalPointerMoveMonitor } from 'vs/base/browser/globalMouseMoveMonitor';
88
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
99
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
10+
import { BrowserFeatures } from 'vs/base/browser/canIUse';
11+
import { isSafari } from 'vs/base/browser/browser';
1012

1113
/**
1214
* Coordinates relative to the whole document (e.g. mouse event's pageX and pageY)
@@ -131,16 +133,58 @@ export class EditorMouseEventFactory {
131133
}
132134
}
133135

136+
export class EditorPointerEventFactory {
137+
138+
private readonly _editorViewDomNode: HTMLElement;
139+
140+
constructor(editorViewDomNode: HTMLElement) {
141+
this._editorViewDomNode = editorViewDomNode;
142+
}
143+
144+
private _create(e: MouseEvent): EditorMouseEvent {
145+
return new EditorMouseEvent(e, this._editorViewDomNode);
146+
}
147+
148+
public onPointerUp(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
149+
return dom.addDisposableListener(target, 'pointerup', (e: MouseEvent) => {
150+
callback(this._create(e));
151+
});
152+
}
153+
154+
public onPointerDown(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
155+
return dom.addDisposableListener(target, 'pointerdown', (e: MouseEvent) => {
156+
callback(this._create(e));
157+
});
158+
}
159+
160+
public onPointerLeave(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
161+
return dom.addDisposableNonBubblingPointerOutListener(target, (e: MouseEvent) => {
162+
callback(this._create(e));
163+
});
164+
}
165+
166+
public onPointerMoveThrottled(target: HTMLElement, callback: (e: EditorMouseEvent) => void, merger: EditorMouseEventMerger, minimumTimeMs: number): IDisposable {
167+
const myMerger: dom.IEventMerger<EditorMouseEvent, MouseEvent> = (lastEvent: EditorMouseEvent, currentEvent: MouseEvent): EditorMouseEvent => {
168+
return merger(lastEvent, this._create(currentEvent));
169+
};
170+
return dom.addDisposableThrottledListener<EditorMouseEvent, MouseEvent>(target, 'pointermove', callback, myMerger, minimumTimeMs);
171+
}
172+
}
173+
134174
export class GlobalEditorMouseMoveMonitor extends Disposable {
135175

136176
private readonly _editorViewDomNode: HTMLElement;
137-
private readonly _globalMouseMoveMonitor: GlobalMouseMoveMonitor<EditorMouseEvent>;
177+
protected readonly _globalMouseMoveMonitor: GlobalMouseMoveMonitor<EditorMouseEvent>;
138178
private _keydownListener: IDisposable | null;
139179

140180
constructor(editorViewDomNode: HTMLElement) {
141181
super();
142182
this._editorViewDomNode = editorViewDomNode;
143-
this._globalMouseMoveMonitor = this._register(new GlobalMouseMoveMonitor<EditorMouseEvent>());
183+
this._globalMouseMoveMonitor = this._register(
184+
BrowserFeatures.pointerEvents
185+
? new GlobalPointerMoveMonitor<EditorMouseEvent>()
186+
: new GlobalMouseMoveMonitor<EditorMouseEvent>()
187+
);
144188
this._keydownListener = null;
145189
}
146190

0 commit comments

Comments
 (0)