Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(web): proper linkage of sources to events 🪠 #10960

Merged
merged 7 commits into from
Apr 3, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export class AsyncClosureDispatchQueue {
this.defaultWaitFactory = defaultWaitFactory || (() => { return timedPromise(0) });
}

get defaultWait() {
return this.defaultWaitFactory();
}

get ready() {
return this.queue.length == 0 && !this.waitLock;
}
Expand Down
52 changes: 23 additions & 29 deletions common/web/gesture-recognizer/src/engine/inputEventEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ export function processSampleClientCoords<Type, StateToken>(config: GestureRecog
} as InputSample<Type, StateToken>;
}

export abstract class InputEventEngine<HoveredItemType, StateToken> extends InputEngineBase<HoveredItemType, StateToken> {
export abstract class InputEventEngine<ItemType, StateToken> extends InputEngineBase<ItemType, StateToken> {
abstract registerEventHandlers(): void;
abstract unregisterEventHandlers(): void;

protected buildSampleFor(clientX: number, clientY: number, target: EventTarget, timestamp: number, source: GestureSource<HoveredItemType, StateToken>): InputSample<HoveredItemType, StateToken> {
const sample: InputSample<HoveredItemType, StateToken> = {
protected buildSampleFor(clientX: number, clientY: number, target: EventTarget, timestamp: number, source: GestureSource<ItemType, StateToken>): InputSample<ItemType, StateToken> {
const sample: InputSample<ItemType, StateToken> = {
...processSampleClientCoords(this.config, clientX, clientY),
t: timestamp,
stateToken: source?.stateToken ?? this.stateToken
Expand All @@ -32,7 +32,7 @@ export abstract class InputEventEngine<HoveredItemType, StateToken> extends Inpu
return sample;
}

protected onInputStart(identifier: number, sample: InputSample<HoveredItemType, StateToken>, target: EventTarget, isFromTouch: boolean) {
protected onInputStart(identifier: number, sample: InputSample<ItemType, StateToken>, target: EventTarget, isFromTouch: boolean) {
const touchpoint = this.createTouchpoint(identifier, isFromTouch);
touchpoint.update(sample);

Expand All @@ -57,46 +57,40 @@ export abstract class InputEventEngine<HoveredItemType, StateToken> extends Inpu
return touchpoint;
}

protected onInputMove(identifier: number, sample: InputSample<HoveredItemType, StateToken>, target: EventTarget) {
const activePoint = this.getTouchpointWithId(identifier);
if(!activePoint) {
protected onInputMove(touchpoint: GestureSource<ItemType, StateToken>, sample: InputSample<ItemType, StateToken>, target: EventTarget) {
if(!touchpoint) {
return;
}

activePoint.update(sample);
try {
touchpoint.update(sample);
} catch(err) {
reportError('Error occurred while updating source', err);
}
}

protected onInputMoveCancel(identifier: number, sample: InputSample<HoveredItemType, StateToken>, target: EventTarget) {
const touchpoint = this.getTouchpointWithId(identifier);
protected onInputMoveCancel(touchpoint: GestureSource<ItemType, StateToken>, sample: InputSample<ItemType, StateToken>, target: EventTarget) {
if(!touchpoint) {
return;
}

touchpoint.update(sample);
touchpoint.path.terminate(true);
try {
touchpoint.update(sample);
touchpoint.path.terminate(true);
} catch(err) {
reportError('Error occurred while cancelling further input for source', err);
}
}

protected onInputEnd(identifier: number, target: EventTarget) {
const touchpoint = this.getTouchpointWithId(identifier);
protected onInputEnd(touchpoint: GestureSource<ItemType, StateToken>, target: EventTarget) {
if(!touchpoint) {
return;
}

const lastEntry = touchpoint.path.stats.lastSample;
const sample = this.buildSampleFor(lastEntry.clientX, lastEntry.clientY, target, lastEntry.t, touchpoint);

/* While an 'end' event immediately follows a 'move' if it occurred simultaneously,
* this is decidedly _not_ the case if the touchpoint was held for a while without
* moving, even at the point of its release.
*
* We'll never need to worry about the touchpoint moving here, and thus we don't
* need to worry about `currentHoveredItem` changing. We're only concerned with
* recording the _timing_ of the touchpoint's release.
*/
if(sample.t != lastEntry.t) {
touchpoint.update(sample);
try {
touchpoint.path.terminate(false);
} catch(err) {
reportError('Error occurred while finalizing input for source', err);
}

this.getTouchpointWithId(identifier)?.path.terminate(false);
}
}
59 changes: 28 additions & 31 deletions common/web/gesture-recognizer/src/engine/mouseEventEngine.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { GestureRecognizerConfiguration } from "./configuration/gestureRecognizerConfiguration.js";
import { InputEventEngine } from "./inputEventEngine.js";
import { InputSample } from "./headless/inputSample.js";
import { Nonoptional } from "./nonoptional.js";
import { ZoneBoundaryChecker } from "./configuration/zoneBoundaryChecker.js";
import { GestureSource } from "./headless/gestureSource.js";

// Does NOT use the AsyncClosureDispatchQueue... simply because there can only ever be one mouse touchpoint.
export class MouseEventEngine<HoveredItemType, StateToken = any> extends InputEventEngine<HoveredItemType, StateToken> {
export class MouseEventEngine<ItemType, StateToken = any> extends InputEventEngine<ItemType, StateToken> {
private readonly _mouseStart: typeof MouseEventEngine.prototype.onMouseStart;
private readonly _mouseMove: typeof MouseEventEngine.prototype.onMouseMove;
private readonly _mouseEnd: typeof MouseEventEngine.prototype.onMouseEnd;

private hasActiveClick: boolean = false;
private disabledSafeBounds: number = 0;

public constructor(config: Nonoptional<GestureRecognizerConfiguration<HoveredItemType, StateToken>>) {
private currentSource: GestureSource<ItemType, StateToken> = null;
private readonly activeIdentifier = 0;

public constructor(config: Nonoptional<GestureRecognizerConfiguration<ItemType, StateToken>>) {
super(config);

// We use this approach, rather than .bind, because _this_ version allows hook
Expand All @@ -26,20 +29,6 @@ export class MouseEventEngine<HoveredItemType, StateToken = any> extends InputEv
private get eventRoot(): HTMLElement {
return this.config.mouseEventRoot;
}
private get activeIdentifier(): number {
return 0;
}

// public static forPredictiveBanner(banner: SuggestionBanner, handlerRoot: SuggestionManager) {
// const config: GestureRecognizerConfiguration = {
// targetRoot: banner.getDiv(),
// // document.body is the event root b/c we need to track the mouse if it leaves
// // the VisualKeyboard's hierarchy.
// eventRoot: document.body,
// };

// return new MouseEventEngine(config);
// }

registerEventHandlers() {
this.eventRoot.addEventListener('mousedown', this._mouseStart, true);
Expand Down Expand Up @@ -67,10 +56,9 @@ export class MouseEventEngine<HoveredItemType, StateToken = any> extends InputEv
}
}

private buildSampleFromEvent(event: MouseEvent, identifier: number) {
private buildSampleFromEvent(event: MouseEvent) {
// WILL be null for newly-starting `GestureSource`s / contact points.
const source = this.getTouchpointWithId(identifier);
return this.buildSampleFor(event.clientX, event.clientY, event.target, performance.now(), source);
return this.buildSampleFor(event.clientX, event.clientY, event.target, performance.now(), this.currentSource);
}

onMouseStart(event: MouseEvent) {
Expand All @@ -82,52 +70,61 @@ export class MouseEventEngine<HoveredItemType, StateToken = any> extends InputEv

this.preventPropagation(event);

const identifier = this.activeIdentifier;
const sample = this.buildSampleFromEvent(event, identifier);
const sample = this.buildSampleFromEvent(event);

if(!ZoneBoundaryChecker.inputStartOutOfBoundsCheck(sample, this.config)) {
// If we started very close to a safe zone border, remember which one(s).
// This is important for input-sequence cancellation check logic.
this.disabledSafeBounds = ZoneBoundaryChecker.inputStartSafeBoundProximityCheck(sample, this.config);
}

this.onInputStart(identifier, sample, event.target, false);
const touchpoint = this.onInputStart(this.activeIdentifier, sample, event.target, false);
this.currentSource = touchpoint;

const cleanup = () => {
this.currentSource = null;
}

touchpoint.path.on('complete', cleanup);
touchpoint.path.on('invalidated', cleanup);
}

onMouseMove(event: MouseEvent) {
if(!this.hasActiveTouchpoint(this.activeIdentifier)) {
const source = this.currentSource;
if(!source) {
return;
}

const sample = this.buildSampleFromEvent(event, this.activeIdentifier);
const sample = this.buildSampleFromEvent(event);

if(!event.buttons) {
if(this.hasActiveClick) {
this.hasActiveClick = false;
this.onInputMoveCancel(this.activeIdentifier, sample, event.target);
this.onInputMoveCancel(source, sample, event.target);
}
return;
}

this.preventPropagation(event);
const config = this.getConfigForId(this.activeIdentifier);
const config = source.currentRecognizerConfig;

if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, config, this.disabledSafeBounds)) {
this.onInputMove(this.activeIdentifier, sample, event.target);
this.onInputMove(source, sample, event.target);
} else {
this.onInputMoveCancel(this.activeIdentifier, sample, event.target);
this.onInputMoveCancel(source, sample, event.target);
}
}

onMouseEnd(event: MouseEvent) {
if(!this.hasActiveTouchpoint(this.activeIdentifier)) {
const source = this.currentSource;
if(!source) {
return;
}

if(!event.buttons) {
this.hasActiveClick = false;
}

this.onInputEnd(this.activeIdentifier, event.target);
this.onInputEnd(source, event.target);
}
}
Loading
Loading