Improve Frontend -> Backend user input system (#348)

Includes refactor that sends coordinates of the document viewports to the backend so input is sent relative to the application window
Closes #124
Fixes #291

* Improve Frontend -> Backend user input system

* Code review changes

* More code review changes

* Fix TS error
This commit is contained in:
Keavon Chambers
2021-08-14 05:38:35 -07:00
parent d82e328a73
commit eff511b00d
9 changed files with 269 additions and 129 deletions

View File

@@ -118,7 +118,7 @@
<CanvasRuler :origin="0" :majorMarkSpacing="100" :direction="RulerDirection.Vertical" />
</LayoutCol>
<LayoutCol :class="'canvas-area'">
<div class="canvas" @mousedown="canvasMouseDown" @mouseup="canvasMouseUp" @mousemove="canvasMouseMove" ref="canvas">
<div class="canvas" ref="canvas">
<svg v-html="viewportSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
</div>
</LayoutCol>
@@ -210,7 +210,6 @@
<script lang="ts">
import { defineComponent } from "vue";
import { makeModifiersBitfield } from "@/utilities/input";
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiveTool, SetCanvasZoom, SetCanvasRotation } from "@/utilities/response-handler";
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
import { comingSoon } from "@/utilities/errors";
@@ -259,25 +258,6 @@ export default defineComponent({
this.canvasSvgWidth = `${width}px`;
this.canvasSvgHeight = `${height}px`;
(await wasm).viewport_resize(width, height);
},
async canvasMouseDown(e: MouseEvent) {
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
(await wasm).on_mouse_down(e.offsetX, e.offsetY, e.buttons, modifiers);
},
async canvasMouseUp(e: MouseEvent) {
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
(await wasm).on_mouse_up(e.offsetX, e.offsetY, e.buttons, modifiers);
},
async canvasMouseMove(e: MouseEvent) {
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
(await wasm).on_mouse_move(e.offsetX, e.offsetY, modifiers);
},
async canvasMouseScroll(e: WheelEvent) {
e.preventDefault();
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
(await wasm).on_mouse_scroll(e.deltaX, e.deltaY, e.deltaZ, modifiers);
},
async setCanvasZoom(newZoom: number) {
(await wasm).set_canvas_zoom(newZoom / 100);
@@ -327,12 +307,8 @@ export default defineComponent({
}
});
// TODO: Move event listeners to `main.ts`
const canvas = this.$refs.canvas as HTMLElement;
canvas.addEventListener("wheel", this.canvasMouseScroll, { passive: false });
window.addEventListener("resize", () => this.viewportResize());
window.addEventListener("DOMContentLoaded", () => this.viewportResize());
window.addEventListener("resize", this.viewportResize);
window.addEventListener("DOMContentLoaded", this.viewportResize);
},
data() {
return {

View File

@@ -1,17 +1,26 @@
import { createApp } from "vue";
import { fullscreenModeChanged } from "@/utilities/fullscreen";
import { handleKeyUp, handleKeyDown, handleMouseDown } from "@/utilities/input";
import { onKeyUp, onKeyDown, onMouseMove, onMouseDown, onMouseUp, onMouseScroll, onWindowResize } from "@/utilities/input";
import "@/utilities/errors";
import App from "@/App.vue";
// Bind global browser events
window.addEventListener("resize", onWindowResize);
window.addEventListener("DOMContentLoaded", onWindowResize);
document.addEventListener("contextmenu", (e) => e.preventDefault());
document.addEventListener("fullscreenchange", () => fullscreenModeChanged());
window.addEventListener("keyup", (e: KeyboardEvent) => handleKeyUp(e));
window.addEventListener("keydown", (e: KeyboardEvent) => handleKeyDown(e));
window.addEventListener("mousedown", (e: MouseEvent) => handleMouseDown(e));
window.addEventListener("keyup", onKeyUp);
window.addEventListener("keydown", onKeyDown);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mousedown", onMouseDown);
window.addEventListener("mouseup", onMouseUp);
window.addEventListener("wheel", onMouseScroll, { passive: false });
// Initialize the Vue application
createApp(App).mount("#app");

View File

@@ -3,7 +3,11 @@ import { dialogIsVisible, dismissDialog, submitDialog } from "@/utilities/dialog
const wasm = import("@/../wasm/pkg");
export function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): boolean {
let viewportMouseInteractionOngoing = false;
// Keyboard events
function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): boolean {
// Don't redirect user input from text entry into HTML elements
const target = e.target as HTMLElement;
if (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable) return false;
@@ -31,10 +35,10 @@ export function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): boolean
return true;
}
export async function handleKeyDown(e: KeyboardEvent) {
export async function onKeyDown(e: KeyboardEvent) {
if (shouldRedirectKeyboardEventToBackend(e)) {
e.preventDefault();
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
const modifiers = makeModifiersBitfield(e);
(await wasm).on_key_down(e.key, modifiers);
return;
}
@@ -48,27 +52,77 @@ export async function handleKeyDown(e: KeyboardEvent) {
}
}
export async function handleKeyUp(e: KeyboardEvent) {
export async function onKeyUp(e: KeyboardEvent) {
if (shouldRedirectKeyboardEventToBackend(e)) {
e.preventDefault();
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
const modifiers = makeModifiersBitfield(e);
(await wasm).on_key_up(e.key, modifiers);
}
}
export async function handleMouseDown(e: MouseEvent) {
// Mouse events
export async function onMouseMove(e: MouseEvent) {
if (!e.buttons) viewportMouseInteractionOngoing = false;
const modifiers = makeModifiersBitfield(e);
(await wasm).on_mouse_move(e.clientX, e.clientY, e.buttons, modifiers);
}
export async function onMouseDown(e: MouseEvent) {
const target = e.target && (e.target as HTMLElement);
const clickedInsideDialog = target && target.closest(".dialog-modal .floating-menu-content");
const inCanvas = target && target.closest(".canvas");
const inDialog = target && target.closest(".dialog-modal .floating-menu-content");
if (dialogIsVisible() && !clickedInsideDialog) {
// Block middle mouse button auto-scroll mode
if (e.button === 1) e.preventDefault();
if (dialogIsVisible() && !inDialog) {
dismissDialog();
e.preventDefault();
e.stopPropagation();
}
if (inCanvas) viewportMouseInteractionOngoing = true;
if (viewportMouseInteractionOngoing) {
const modifiers = makeModifiersBitfield(e);
(await wasm).on_mouse_down(e.clientX, e.clientY, e.buttons, modifiers);
}
}
export function makeModifiersBitfield(control: boolean, shift: boolean, alt: boolean): number {
// eslint-disable-next-line no-bitwise
return Number(control) | (Number(shift) << 1) | (Number(alt) << 2);
export async function onMouseUp(e: MouseEvent) {
if (!e.buttons) viewportMouseInteractionOngoing = false;
const modifiers = makeModifiersBitfield(e);
(await wasm).on_mouse_up(e.clientX, e.clientY, e.buttons, modifiers);
}
export async function onMouseScroll(e: WheelEvent) {
const target = e.target && (e.target as HTMLElement);
const inCanvas = target && target.closest(".canvas");
if (inCanvas) {
e.preventDefault();
const modifiers = makeModifiersBitfield(e);
(await wasm).on_mouse_scroll(e.clientX, e.clientY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers);
}
}
export async function onWindowResize() {
const viewports = Array.from(document.querySelectorAll(".canvas"));
const boundsOfViewports = viewports.map((canvas) => {
const bounds = canvas.getBoundingClientRect();
return [bounds.left, bounds.top, bounds.right, bounds.bottom];
});
const flattened = boundsOfViewports.flat();
const data = Float64Array.from(flattened);
if (boundsOfViewports.length > 0) (await wasm).bounds_of_viewports(data);
}
export function makeModifiersBitfield(e: MouseEvent | KeyboardEvent): number {
// eslint-disable-next-line no-bitwise
return Number(e.ctrlKey) | (Number(e.shiftKey) << 1) | (Number(e.altKey) << 2);
}