import { useSyncExternalStore } from 'react';
import { SimpleEventEmitter } from '@mary-ext/simple-event-emitter';
import { MODIFIERS } from '@mary/keybinds';
import { IS_TOUCH_DEVICE } from '#/lib/browser/platform';
/** the kind of input the user is currently driving the ui with */
export type InputModality = 'keyboard' | 'mouse' | 'pen' | 'touch';
const emitter = new SimpleEventEmitter<[]>();
let modality: InputModality = IS_TOUCH_DEVICE ? 'touch' : 'mouse';
const setModality = (next: InputModality) => {
if (next !== modality) {
modality = next;
emitter.emit();
}
};
const onPointerEvent = (evt: PointerEvent) => {
setModality(evt.pointerType === 'touch' || evt.pointerType === 'pen' ? evt.pointerType : 'mouse');
};
const onKeyDown = (evt: KeyboardEvent) => {
if (MODIFIERS.includes(evt.key)) {
return;
}
setModality('keyboard');
};
const subscribe = (listener: () => void) => {
if (!emitter.hasListeners()) {
window.addEventListener('pointerdown', onPointerEvent, { capture: true, passive: true });
window.addEventListener('pointermove', onPointerEvent, { capture: true, passive: true });
window.addEventListener('keydown', onKeyDown, { capture: true, passive: true });
}
const unsubscribe = emitter.subscribe(listener);
return () => {
unsubscribe();
if (!emitter.hasListeners()) {
window.removeEventListener('pointerdown', onPointerEvent, { capture: true });
window.removeEventListener('pointermove', onPointerEvent, { capture: true });
window.removeEventListener('keydown', onKeyDown, { capture: true });
}
};
};
const getSnapshot = () => modality;
/**
* Reactive input modality hook, for behavior that has to follow the input in use rather than what the device
* is capable of.
*
* @returns the input modality of the most recent interaction
*/
export const useInputModality = (): InputModality => {
return useSyncExternalStore(subscribe, getSnapshot);
};