lazy-widgets
Version:
Typescript retained mode GUI for the HTML canvas API
393 lines • 17.1 kB
JavaScript
import { PointerWheelEvent, PointerWheelMode } from '../events/PointerWheelEvent.js';
import { PointerReleaseEvent } from '../events/PointerReleaseEvent.js';
import { PointerEvent } from '../events/PointerEvent.js';
import { PointerPressEvent } from '../events/PointerPressEvent.js';
import { PointerMoveEvent } from '../events/PointerMoveEvent.js';
import { PointerHint } from './PointerHint.js';
import { LeaveRootEvent } from '../events/LeaveRootEvent.js';
/**
* A generic pointer {@link Driver | driver}.
*
* Does nothing on its own, but provides an API for sending pointer events to
* registered roots and (un)registering pointers.
*
* @category Driver
*/
export class PointerDriver {
constructor() {
/**
* The current state for each registered and enabled root. Contains whether
* each root is pressing, hovering, and which pointer is bound to it
*/
this.states = new Map();
/**
* The next available pointer ID. See {@link PointerDriver#registerPointer}
*/
this.nextPointerID = 0;
/**
* The {@link PointerHint | hints} for each pointer. The keys are pointer
* IDs, while the values are that pointer's hint.
*
* See {@link PointerDriver#getPointerHint}
*/
this.hints = new Map();
/**
* The dragToScroll value of every pointer ID. See
* {@link PointerDriver#registerPointer}.
*/
this.dragToScroll = new Map();
}
/** Unassign a pointer from a given root and its state. */
unassignPointer(root, state) {
// Clear pointer state
const pointerID = state.pointer;
if (state.pointer !== null) {
this.setPointerHint(state.pointer, PointerHint.None);
}
// Clear state
state.pointer = null;
if (state.hovering) {
// Dispatch LeaveRootEvent event if hovering
this.dispatchEvent(root, state, new LeaveRootEvent(), pointerID === null ? null : [this, pointerID]);
}
state.hovering = false;
state.pressing = 0;
state.dragLast = null;
}
/**
* Register a new pointer.
*
* @param dragToScroll - If true, then dragging will result in PointerWheelEvent events if no widget captures the events.
* @returns Returns {@link PointerDriver#nextPointerID} and increments it
*/
registerPointer(dragToScroll = false) {
const newID = this.nextPointerID++;
this.setPointerHint(newID, PointerHint.None);
this.dragToScroll.set(newID, dragToScroll);
return newID;
}
/**
* Unregister a pointer.
*
* If a root has this pointer bound to it, the pointer is unbound from the
* root, a LeaveRootEvent event is dispatched to the root and the hovering
* and pressing state of the root is set to false.
*/
unregisterPointer(pointer) {
for (const [root, state] of this.states) {
// Unassign pointer if unregistered pointer was assigned to root
if (state.pointer === pointer) {
this.unassignPointer(root, state);
}
}
this.hints.delete(pointer);
this.dragToScroll.delete(pointer);
}
/**
* Check if a given pointer can dispatch an event to a given root. Also
* automatically assigns pointer to root if possible. For internal use only.
*
* @param state - The root's state. Although the function could technically get the state itself, it's passed to avoid repetition since you will need the state yourself
* @param givingActiveInput - Is the pointer giving active input (pressing button or scrolling)? If so, then it can auto-assign if the root is not being pressed by another pointer
*/
canDispatchEvent(root, pointer, state, givingActiveInput) {
// If there is no pointer assigned, assign this one
const firstAssign = state.pointer === null;
if (firstAssign) {
state.pointer = pointer;
}
// If pointer is entering this root for the first time, then find which
// root the pointer was assigned to and dispatch a leave event
const pointerMatches = state.pointer === pointer;
if (!pointerMatches || firstAssign) {
for (const [otherRoot, otherState] of this.states) {
// Ignore if its this root
if (otherRoot === root) {
continue;
}
// If other root has this pointer assigned, unassign it
if (otherState.pointer === pointer) {
this.unassignPointer(otherRoot, otherState);
}
}
}
// Ignore if pointer is not the assigned one and not giving active input
// or being pressed by the assigned pointer
if (!pointerMatches && (!givingActiveInput || state.pressing > 0)) {
return false;
}
else {
// Replace assigned pointer and clear old assigned pointer's hint if
// pointer changed and giving active input
if (givingActiveInput && state.pointer !== pointer) {
this.unassignPointer(root, state);
state.pointer = pointer;
}
return true;
}
}
/** Denormalise normalised pointer coordinates. Internal use only. */
denormaliseCoords(root, xNorm, yNorm) {
const [width, height] = root.dimensions;
return [xNorm * width, yNorm * height];
}
/**
* Dispatch a pointer event to a given root. The type of
* {@link PointerEvent} is decided automatically based on the root's state
* and whether its pressing or not.
*
* If null, the last pressing state is used, meaning that the pressing state
* has not changed. Useful if getting pointer movement in an event based
* environment where you only know when a pointer press occurs, but not if
* the pointer is pressed or not
*
* @param pointer - The registered pointer ID
* @param xNorm - The normalised (non-integer range from 0 to 1) X coordinate of the pointer event. 0 is the left edge of the root, while 1 is the right edge of the root.
* @param yNorm - The normalised (non-integer range from 0 to 1) Y coordinate of the pointer event. 0 is the top edge of the root, while 1 is the bottom edge of the root.
* @param pressing - Is the pointer pressed? If null, then the last pressing state will be used. A bitmask where each set bit represents a different button being pressed
* @param shift - Is shift being pressed?
* @param ctrl - Is control being pressed?
* @param alt - Is alt being pressed?
* @returns Returns true if the pointer event was captured.
*/
movePointer(root, pointer, xNorm, yNorm, pressing, shift, ctrl, alt) {
const state = this.states.get(root);
if (state === undefined) {
console.warn('PointerDriver was not registered to Root, but tried to dispatch an event to it');
return false;
}
// If press state was not supplied, then it hasn't changed. Use the last
// state
if (pressing === null) {
pressing = state.pressing;
}
// Abort if this pointer can't dispatch an event to the target root
if (!this.canDispatchEvent(root, pointer, state, pressing > 0)) {
return false;
}
// Update state and dispatch event
state.hovering = true;
const [x, y] = this.denormaliseCoords(root, xNorm, yNorm);
let captured = false;
const source = [this, pointer];
if (pressing !== state.pressing) {
// Get how many bits in the bitmask you need to check
const bits = Math.floor(Math.log2(Math.max(pressing, state.pressing)));
// Check which buttons changed and generate an event for each
for (let bit = 0; bit <= bits; bit++) {
const wasPressed = ((state.pressing >> bit) & 0x1) === 1;
const isPressed = ((pressing >> bit) & 0x1) === 1;
if (wasPressed === isPressed) {
continue;
}
captured || (captured = this.dispatchEvent(root, state, new (isPressed ? PointerPressEvent : PointerReleaseEvent)(x, y, bit, shift, ctrl, alt, source), source));
}
state.pressing = pressing;
}
else {
captured = this.dispatchEvent(root, state, new PointerMoveEvent(x, y, shift, ctrl, alt, source), source);
}
// Update pointer's hint
// XXX an event listener might have removed the driver or disabled the
// root, so we have to check again to prevent the pointer hint from
// being set while the driver is not being attached to the root
if (this.states.has(root)) {
if (state.pressing > 0) {
this.setPointerHint(pointer, PointerHint.Pressing);
}
else {
this.setPointerHint(pointer, PointerHint.Hovering);
}
}
return captured;
}
/**
* Dispatch a {@link LeaveRootEvent} event to a given root. Event will only
* be dispatched if the root was being hovered.
*
* @param pointer - The registered pointer ID
* @returns Returns true if the event was captured
*/
leavePointer(root, pointer) {
const state = this.states.get(root);
if (state === undefined) {
console.warn('PointerDriver was not registered to Root, but tried to dispatch an event to it');
return false;
}
// Dispatch leave event if this is the assigned pointer and if hovering
if (state.hovering && state.pointer == pointer) {
state.hovering = false;
state.pressing = 0;
state.dragLast = null;
const captured = this.dispatchEvent(root, state, new LeaveRootEvent(), [this, pointer]);
this.setPointerHint(pointer, PointerHint.None);
return captured;
}
else {
return false;
}
}
/**
* Dispatch a {@link LeaveRootEvent} event to any root with the given
* pointer assigned. Event will only be dispatched if the root was being
* hovered. Pointer will also be unassigned from root.
*
* @param pointer - The registered pointer ID
*/
leaveAnyPointer(pointer) {
for (const root of this.states.keys()) {
this.leavePointer(root, pointer);
}
}
/**
* Dispatch a mouse wheel event in a given 2D direction. Event will only be
* dispatched if the root was being hovered.
*
* @param pointer - The registered pointer ID
* @param xNorm - The normalised (non-integer range from 0 to 1) X coordinate of the pointer event. 0 is the left edge of the root, while 1 is the right edge of the root.
* @param yNorm - The normalised (non-integer range from 0 to 1) Y coordinate of the pointer event. 0 is the top edge of the root, while 1 is the bottom edge of the root.
* @param deltaX - How much was scrolled horizontally, in pixels
* @param deltaY - How much was scrolled vertically, in pixels
* @param deltaZ - How much was scrolled in the Z axis, in pixels. Rarely used
* @param deltaMode - How the delta values should be interpreted
* @param shift - Is shift being pressed?
* @param ctrl - Is control being pressed?
* @param alt - Is alt being pressed?
* @returns Returns true if the pointer event was captured.
*/
wheelPointer(root, pointer, xNorm, yNorm, deltaX, deltaY, deltaZ, deltaMode, shift, ctrl, alt) {
const state = this.states.get(root);
if (state === undefined) {
console.warn('PointerDriver was not registered to Root, but tried to dispatch an event to it');
return false;
}
// Abort if this pointer can't dispatch an event to the target root
if (!this.canDispatchEvent(root, pointer, state, true)) {
return false;
}
// Update state and dispatch event
state.hovering = true;
const [x, y] = this.denormaliseCoords(root, xNorm, yNorm);
const source = [this, pointer];
return this.dispatchEvent(root, state, new PointerWheelEvent(x, y, deltaX, deltaY, deltaZ, deltaMode, false, shift, ctrl, alt, source), source);
}
/**
* Set a pointer's {@link PointerHint | hint}.
*
* @param pointer - The registered pointer ID
* @param hint - The new pointer hint
* @returns Returns true if the pointer hint changed, else, false
*/
setPointerHint(pointer, hint) {
const oldHint = this.hints.get(pointer);
if (oldHint !== hint) {
this.hints.set(pointer, hint);
return true;
}
else {
return false;
}
}
/**
* Get a pointer's {@link PointerHint | hint}.
*
* @param pointer - The registered pointer ID
*
* @returns Returns the given pointer ID's hint. If the pointer ID is not registered, {@link PointerHint.None} is returned.
*/
getPointerHint(pointer) {
var _a;
return (_a = this.hints.get(pointer)) !== null && _a !== void 0 ? _a : PointerHint.None;
}
/**
* Creates a state for the enabled root in {@link PointerDriver#states}.
*/
onEnable(root) {
if (this.states.has(root)) {
console.warn('PointerDriver was already registered to the Root, but "onEnable" was called');
return;
}
// Create new state for UI that just got enabled
this.states.set(root, {
pointer: null,
pressing: 0,
hovering: false,
dragLast: null,
dragOrigin: [0, 0],
});
}
/**
* Dispatches a leave-root event for the disabled root and deletes the state
* of the disabled root from {@link PointerDriver#states}.
*/
onDisable(root) {
if (!this.states.has(root)) {
console.warn('PointerDriver was not registered to the Root, but "onDisable" was called');
return;
}
// Dispatch leave-root event
root.dispatchEvent(new LeaveRootEvent());
// Reset hint for assigned pointer and stop dragging
const state = this.states.get(root);
if (state !== undefined && state.pointer !== null) {
this.setPointerHint(state.pointer, PointerHint.None);
state.dragLast = null;
}
// Delete state for UI thats about to get disabled
this.states.delete(root);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
update(_root) { }
/**
* Dispatch an event to a root.
*
* @returns Returns true if the event was captured
*/
dispatchEvent(root, state, event, source) {
// Check if drag to scroll is enabled for this root
const dragToScroll = state.pointer === null
? false
: this.dragToScroll.get(state.pointer);
// If this is a pointer event and pointer is dragging, continue
// doing dragging logic
if (state.dragLast !== null && event instanceof PointerEvent) {
const [startX, startY] = state.dragLast;
const capturedList = root.dispatchEvent(new PointerWheelEvent(...state.dragOrigin, startX - event.x, startY - event.y, 0, PointerWheelMode.Pixel, true, false, false, true, source));
let captured = false;
for (const [capEvent, eventCaptured] of capturedList) {
if (eventCaptured && capEvent.isa(PointerWheelEvent)) {
captured = true;
break;
}
}
if (event.isa(PointerReleaseEvent)) {
state.dragLast = null;
}
else {
state.dragLast[0] = event.x;
state.dragLast[1] = event.y;
}
return captured;
}
// Dispatch event. If nobody captures the event, dragToScroll is
// enabled and this is a pointer press, then start dragging
const capList = root.dispatchEvent(event);
for (const [_capturer, captured] of capList) {
if (captured) {
state.dragLast = null;
return true;
}
}
if (dragToScroll && event.isa(PointerPressEvent)) {
state.dragLast = [event.x, event.y];
state.dragOrigin[0] = event.x;
state.dragOrigin[1] = event.y;
}
return false;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onFocusChanged(_root, _focusType, _newFocus) { }
// eslint-disable-next-line @typescript-eslint/no-empty-function
onFocusCapturerChanged(_root, _focusType, _oldCapturer, _newCapturer) { }
}
//# sourceMappingURL=PointerDriver.js.map