openOutpaint/js/input.js
Victor Seiji Hariki f5b0369476 finally fix shortcuts triggering on prompt fields
Basically checks if focus is an input field, and transfer focus to
window as soon as mouse hovers over the canvas

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>

Former-commit-id: 5ce4877c69708a1ea981d9dac4b9102048152e58
2022-11-23 08:24:18 -03:00

421 lines
9.6 KiB
JavaScript

const inputConfig = {
clickRadius: 10, // Radius to be considered a click (pixels). If farther, turns into a drag
clickTiming: 500, // Timing window to be considered a click (ms). If longer, turns into a drag
dClickTiming: 500, // Timing window to be considered a double click (ms).
keyboardHoldTiming: 100, // Timing window after which to consider holding a key (ms)
};
/**
* Mouse input processing
*/
// Base object generator functions
function _context_coords() {
return {
dragging: {
left: null,
middle: null,
right: null,
},
prev: {
x: 0,
y: 0,
},
pos: {
x: 0,
y: 0,
},
};
}
function _mouse_observers() {
return {
// Simple click handler
onclick: new Observer(),
// Double click handler (will still trigger simple click handler as well)
ondclick: new Observer(),
// Drag handler
ondragstart: new Observer(),
ondrag: new Observer(),
ondragend: new Observer(),
// Paint handler (like drag handler, but with no delay); will trigger during clicks too
onpaintstart: new Observer(),
onpaint: new Observer(),
onpaintend: new Observer(),
};
}
function _context_observers() {
return {
onwheel: new Observer(),
onmousemove: new Observer(),
left: _mouse_observers(),
middle: _mouse_observers(),
right: _mouse_observers(),
};
}
const mouse = {
buttons: {
right: null,
left: null,
middle: null,
},
// Mouse Actions in Window Coordinates
window: _context_coords(),
// Mouse Actions in Canvas Coordinates
canvas: _context_coords(),
// Mouse Actions in World Coordinates
world: _context_coords(),
listen: {
window: _context_observers(),
canvas: _context_observers(),
world: _context_observers(),
},
};
function _mouse_state_snapshot() {
return {
buttons: window.structuredClone(mouse.buttons),
window: window.structuredClone(mouse.window),
canvas: window.structuredClone(mouse.canvas),
world: window.structuredClone(mouse.world),
};
}
const _double_click_timeout = {};
const _drag_start_timeout = {};
window.onmousedown = (evn) => {
const time = new Date();
// Processes for a named button
const onhold = (key) => () => {
if (_double_click_timeout[key]) {
// ondclick event
["window", "canvas", "world"].forEach((ctx) =>
mouse.listen[ctx][key].ondclick.emit({
target: evn.target,
buttonId: evn.button,
x: mouse[ctx].pos.x,
y: mouse[ctx].pos.y,
timestamp: new Date(),
})
);
} else {
// Start timer
_double_click_timeout[key] = setTimeout(
() => delete _double_click_timeout[key],
inputConfig.dClickTiming
);
}
// Set drag start timeout
_drag_start_timeout[key] = setTimeout(() => {
["window", "canvas", "world"].forEach((ctx) => {
mouse.listen[ctx][key].ondragstart.emit({
target: evn.target,
buttonId: evn.button,
x: mouse[ctx].pos.x,
y: mouse[ctx].pos.y,
timestamp: new Date(),
});
if (mouse[ctx].dragging[key]) mouse[ctx].dragging[key].drag = true;
delete _drag_start_timeout[key];
});
}, inputConfig.clickTiming);
["window", "canvas", "world"].forEach((ctx) => {
mouse.buttons[key] = time;
mouse[ctx].dragging[key] = {target: evn.target};
Object.assign(mouse[ctx].dragging[key], mouse[ctx].pos);
// onpaintstart event
mouse.listen[ctx][key].onpaintstart.emit({
target: evn.target,
buttonId: evn.button,
x: mouse[ctx].pos.x,
y: mouse[ctx].pos.y,
timestamp: new Date(),
});
});
};
// Runs the correct handler
const buttons = [onhold("left"), onhold("middle"), onhold("right")];
buttons[evn.button] && buttons[evn.button]();
};
window.onmouseup = (evn) => {
const time = new Date();
// Processes for a named button
const onrelease = (key) => () => {
["window", "canvas", "world"].forEach((ctx) => {
const start = {
x: mouse[ctx].dragging[key].x,
y: mouse[ctx].dragging[key].y,
};
// onclick event
const dx = mouse[ctx].pos.x - start.x;
const dy = mouse[ctx].pos.y - start.y;
if (
time.getTime() - mouse.buttons[key].getTime() <
inputConfig.clickTiming &&
dx * dx + dy * dy < inputConfig.clickRadius * inputConfig.clickRadius
)
mouse.listen[ctx][key].onclick.emit({
target: evn.target,
buttonId: evn.button,
x: mouse[ctx].pos.x,
y: mouse[ctx].pos.y,
timestamp: new Date(),
});
// onpaintend event
mouse.listen[ctx][key].onpaintend.emit({
target: evn.target,
initialTarget: mouse[ctx].dragging[key].target,
buttonId: evn.button,
x: mouse[ctx].pos.x,
y: mouse[ctx].pos.y,
timestamp: new Date(),
});
// ondragend event
if (mouse[ctx].dragging[key].drag)
mouse.listen[ctx][key].ondragend.emit({
target: evn.target,
initialTarget: mouse[ctx].dragging[key].target,
buttonId: evn.button,
x: mouse[ctx].pos.x,
y: mouse[ctx].pos.y,
timestamp: new Date(),
});
mouse[ctx].dragging[key] = null;
});
if (_drag_start_timeout[key] !== undefined) {
clearTimeout(_drag_start_timeout[key]);
delete _drag_start_timeout[key];
}
mouse.buttons[key] = null;
};
// Runs the correct handler
const buttons = [onrelease("left"), onrelease("middle"), onrelease("right")];
buttons[evn.button] && buttons[evn.button]();
};
window.onmousemove = (evn) => {
// Set Window Coordinates
Object.assign(mouse.window.prev, mouse.window.pos);
mouse.window.pos = {x: evn.clientX, y: evn.clientY};
// Set Canvas Coordinates (using overlay canvas as reference)
if (evn.target.id === "overlayCanvas") {
Object.assign(mouse.canvas.prev, mouse.canvas.pos);
mouse.canvas.pos = {x: evn.layerX, y: evn.layerY};
}
// Set World Coordinates (For now the same as canvas coords; Will be useful with infinite canvas)
if (evn.target.id === "overlayCanvas") {
Object.assign(mouse.world.prev, mouse.world.pos);
mouse.world.pos = {x: evn.layerX, y: evn.layerY};
}
["window", "canvas", "world"].forEach((ctx) => {
mouse.listen[ctx].onmousemove.emit({
target: evn.target,
px: mouse[ctx].prev.x,
py: mouse[ctx].prev.y,
x: mouse[ctx].pos.x,
y: mouse[ctx].pos.y,
timestamp: new Date(),
});
["left", "middle", "right"].forEach((key) => {
// ondrag event
if (mouse[ctx].dragging[key] && mouse[ctx].dragging[key].drag)
mouse.listen[ctx][key].ondrag.emit({
target: evn.target,
initialTarget: mouse[ctx].dragging[key].target,
px: mouse[ctx].prev.x,
py: mouse[ctx].prev.y,
x: mouse[ctx].pos.x,
y: mouse[ctx].pos.y,
timestamp: new Date(),
});
// onpaint event
if (mouse[ctx].dragging[key])
mouse.listen[ctx][key].onpaint.emit({
target: evn.target,
initialTarget: mouse[ctx].dragging[key].target,
px: mouse[ctx].prev.x,
py: mouse[ctx].prev.y,
x: mouse[ctx].pos.x,
y: mouse[ctx].pos.y,
timestamp: new Date(),
});
});
});
};
window.addEventListener(
"wheel",
(evn) => {
evn.preventDefault();
["window", "canvas", "world"].forEach((ctx) => {
mouse.listen[ctx].onwheel.emit({
target: evn.target,
delta: evn.deltaY,
deltaX: evn.deltaX,
deltaY: evn.deltaY,
deltaZ: evn.deltaZ,
mode: evn.deltaMode,
x: mouse[ctx].pos.x,
y: mouse[ctx].pos.y,
timestamp: new Date(),
});
});
},
{passive: false}
);
/**
* Keyboard input processing
*/
// Base object generator functions
const keyboard = {
keys: {},
isPressed(code) {
return this.keys[key].pressed;
},
isHeld(code) {
return !!this;
},
shortcuts: {},
onShortcut(shortcut, callback) {
/**
* Adds a shortcut handler (shorcut must be in format: {ctrl?: bool, alt?: bool, shift?: bool, key: string (code)})
* key must be the "code" parameter from keydown event; A key is "KeyA" for example
*/
if (this.shortcuts[shortcut.key] === undefined)
this.shortcuts[shortcut.key] = [];
this.shortcuts[shortcut.key].push({
ctrl: shortcut.ctrl,
alt: shortcut.alt,
shift: shortcut.shift,
id: guid(),
callback,
});
},
deleteShortcut(id) {
this.shortcuts.keys().forEach((key) => {
this.shortcuts[key] = this.shortcuts[key].filter((v) => v.id !== id);
});
},
listen: {
onkeydown: new Observer(),
onkeyup: new Observer(),
onkeyholdstart: new Observer(),
onkeyholdend: new Observer(),
onkeyclick: new Observer(),
onshortcut: new Observer(),
},
};
window.onkeydown = (evn) => {
keyboard.listen.onkeydown.emit({
code: evn.code,
key: evn.key,
evn,
});
keyboard.keys[evn.code] = {
pressed: true,
held: false,
_hold_to: setTimeout(() => {
keyboard.keys[evn.code].held = true;
delete keyboard.keys[evn.code]._hold_to;
keyboard.listen.onkeyholdstart.emit({
code: evn.code,
key: evn.key,
evn,
});
}, inputConfig.keyboardHoldTiming),
};
// Process shortcuts if input target is not a text field
switch (evn.target.tagName.toLowerCase()) {
case "input":
case "textarea":
case "select":
case "button":
return; // If in an input field, do not process shortcuts
default:
// Do nothing
break;
}
const callbacks = keyboard.shortcuts[evn.code];
if (callbacks)
callbacks.forEach((callback) => {
if (
!!callback.ctrl === evn.ctrlKey &&
!!callback.alt === evn.altKey &&
!!callback.shift === evn.shiftKey
) {
keyboard.listen.onshortcut.emit({
code: evn.code,
key: evn.key,
id: callback.id,
evn,
});
callback.callback(evn);
}
});
};
window.onkeyup = (evn) => {
keyboard.listen.onkeyup.emit({
code: evn.code,
key: evn.key,
evn,
});
if (keyboard.keys[evn.code] && keyboard.keys[evn.code].held) {
keyboard.listen.onkeyholdend.emit({
code: evn.code,
key: evn.key,
evn,
});
} else {
keyboard.listen.onkeyclick.emit({
code: evn.code,
key: evn.key,
evn,
});
}
keyboard.keys[evn.code] = {
pressed: false,
held: false,
};
};