Rework wasm initialization and reduce global state (#379)

* wasm: do the async initialization only once

This allows the rest of the app to access wasm synchronously.

This allows removing of a global.

* provide the wasm via vue provide/inject.

There's still code directly accessing the wasm. That will be changed later.

* MenuBarInput: use injected wasm instead of the global instance

* Let the App handle event listeners

* move stateful modules into state/

* state/fullscreen: create per instance

* App: load the initial document list on mount.
This got lost a few commits ago. Now it's back.

* state/dialog: create per instance

* util/input: remove dependency on global dialog instance

* state/documents: create per instance

* reponse-handler: move into EditorWasm

* comingSoon: move into dialog

* wasm: allow instantiating multiple editors

* input handlers: do not look at canvases outside the mounted App

* input: listen on the container instead of the window when possible

* - removed proxy from wasm-loader
- integrated with js-dispatcher
- state functions to classes
- integrated some upstream changes

* fix errors caused by merge

* Getting closer:
- added global state to track all instances
- fix fullscreen close trigger
- wasm-loader is statefull
- panic across instanes

* - fix outline while using editor
- removed circular import rule
- added editorInstance to js message constructor

* - changed input handler to a class
- still need a better way of handeling it in App.vue

* - fixed single instance of inputManager to weakmap

* - fix no-explicit-any in a few places
- removed global state from input.ts

* simplified two long lines

* removed global state

* removed $data from App

* add mut self to functions in api.rs

* Update Workspace.vue

remove outdated import

* fixed missing import

* Changes throughout code review; note this causes some bugs to be fixed in a later commit

* PR review round 1

* - fix coming soon bugs
- changed folder structure

* moved declaration to .d.ts

* - changed from classes to functions
- moved decs back to app.vue

* removed need to export js function to rust

* changed folder structure

* fixed indentation breaking multiline strings

* Fix eslint rule to whitelist @/../

* Simplify strip-indents implementation

* replace type assertions with better annotations or proper runtime checks

* Small tweaks and code rearranging improvements after second code review pass

* maybe fix mouse events

* Add back preventDefault for mouse scroll

* code review round 2

* Comment improvements

* -removed runtime checks
- fixed layers not showing

* - extened proxy to cover classes
- stopped multiple panics from logging
- Stop wasm-bindgen from mut ref counting our struct

* cleaned up messageConstructors exports

* Fix input and fullscreen regressions

Co-authored-by: Max Fisher <maxmfishernj@gmail.com>
Co-authored-by: mfish33 <32677537+mfish33@users.noreply.github.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Christian Authmann
2021-12-19 22:37:19 -08:00
committed by Keavon Chambers
co-authored by Max Fisher mfish33 Keavon Chambers
parent 3c1ed1a235
commit 0aefa1bb07
39 changed files with 1524 additions and 1412 deletions
+178
View File
@@ -0,0 +1,178 @@
import { DialogState } from "@/state/dialog";
import { FullscreenState } from "@/state/fullscreen";
import { DocumentsState } from "@/state/documents";
import { EditorState } from "@/state/wasm-loader";
type EventName = keyof HTMLElementEventMap | keyof WindowEventHandlersEventMap;
interface EventListenerTarget {
addEventListener: typeof window.addEventListener;
removeEventListener: typeof window.removeEventListener;
}
export function createInputManager(editor: EditorState, container: HTMLElement, dialog: DialogState, document: DocumentsState, fullscreen: FullscreenState) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: boolean | AddEventListenerOptions }[] = [
{ target: window, eventName: "resize", action: () => onWindowResize(container) },
{ target: window, eventName: "beforeunload", action: (e) => onBeforeUnload(e) },
{ target: window.document, eventName: "contextmenu", action: (e) => e.preventDefault() },
{ target: window.document, eventName: "fullscreenchange", action: () => fullscreen.fullscreenModeChanged() },
{ target: window, eventName: "keyup", action: (e) => onKeyUp(e) },
{ target: window, eventName: "keydown", action: (e) => onKeyDown(e) },
{ target: window, eventName: "mousemove", action: (e) => onMouseMove(e) },
{ target: window, eventName: "mousedown", action: (e) => onMouseDown(e) },
{ target: window, eventName: "mouseup", action: (e) => onMouseUp(e) },
{ target: window, eventName: "wheel", action: (e) => onMouseScroll(e), options: { passive: false } },
];
let viewportMouseInteractionOngoing = false;
const shouldRedirectKeyboardEventToBackend = (e: KeyboardEvent): boolean => {
// Don't redirect user input from text entry into HTML elements
const { target } = e;
if (target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable)) return false;
// Don't redirect when a modal is covering the workspace
if (dialog.dialogIsVisible()) return false;
// Don't redirect a fullscreen request
if (e.key.toLowerCase() === "f11" && e.type === "keydown" && !e.repeat) {
e.preventDefault();
fullscreen.toggleFullscreen();
return false;
}
// Don't redirect a reload request
if (e.key.toLowerCase() === "f5") return false;
// Don't redirect debugging tools
if (e.key.toLowerCase() === "f12") return false;
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "c") return false;
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "i") return false;
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "j") return false;
// Redirect to the backend
return true;
};
const onKeyDown = (e: KeyboardEvent) => {
if (shouldRedirectKeyboardEventToBackend(e)) {
e.preventDefault();
const modifiers = makeModifiersBitfield(e);
editor.instance.on_key_down(e.key, modifiers);
return;
}
if (dialog.dialogIsVisible()) {
if (e.key === "Escape") dialog.dismissDialog();
if (e.key === "Enter") {
dialog.submitDialog();
// Prevent the Enter key from acting like a click on the last clicked button, which might reopen the dialog
e.preventDefault();
}
}
};
const onKeyUp = (e: KeyboardEvent) => {
if (shouldRedirectKeyboardEventToBackend(e)) {
e.preventDefault();
const modifiers = makeModifiersBitfield(e);
editor.instance.on_key_up(e.key, modifiers);
}
};
const onMouseMove = (e: MouseEvent) => {
if (!e.buttons) viewportMouseInteractionOngoing = false;
const modifiers = makeModifiersBitfield(e);
editor.instance.on_mouse_move(e.clientX, e.clientY, e.buttons, modifiers);
};
const onMouseDown = (e: MouseEvent) => {
const { target } = e;
const inCanvas = target instanceof Element && target.closest(".canvas");
const inDialog = target instanceof Element && target.closest(".dialog-modal .floating-menu-content");
// Block middle mouse button auto-scroll mode
if (e.button === 1) e.preventDefault();
if (dialog.dialogIsVisible() && !inDialog) {
dialog.dismissDialog();
e.preventDefault();
e.stopPropagation();
}
if (inCanvas) viewportMouseInteractionOngoing = true;
if (viewportMouseInteractionOngoing) {
const modifiers = makeModifiersBitfield(e);
editor.instance.on_mouse_down(e.clientX, e.clientY, e.buttons, modifiers);
}
};
const onMouseUp = (e: MouseEvent) => {
if (!e.buttons) viewportMouseInteractionOngoing = false;
const modifiers = makeModifiersBitfield(e);
editor.instance.on_mouse_up(e.clientX, e.clientY, e.buttons, modifiers);
};
const onMouseScroll = (e: WheelEvent) => {
const { target } = e;
const inCanvas = target instanceof Element && target.closest(".canvas");
const horizontalScrollableElement = target instanceof Element && target.closest(".scrollable-x");
if (horizontalScrollableElement && e.deltaY !== 0) {
horizontalScrollableElement.scrollTo(horizontalScrollableElement.scrollLeft + e.deltaY, 0);
return;
}
if (inCanvas) {
e.preventDefault();
const modifiers = makeModifiersBitfield(e);
editor.instance.on_mouse_scroll(e.clientX, e.clientY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers);
}
};
const onWindowResize = (container: HTMLElement) => {
const viewports = Array.from(container.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) editor.instance.bounds_of_viewports(data);
};
const onBeforeUnload = (e: BeforeUnloadEvent) => {
const allDocumentsSaved = document.state.documents.reduce((acc, doc) => acc && doc.isSaved, true);
if (!allDocumentsSaved) {
e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?";
e.preventDefault();
}
};
const addListeners = () => {
listeners.forEach(({ target, eventName, action, options }) => target.addEventListener(eventName, action, options));
};
const removeListeners = () => {
listeners.forEach(({ target, eventName, action }) => target.removeEventListener(eventName, action));
};
// Run on creation
addListeners();
onWindowResize(container);
return {
removeListeners,
};
}
export type InputManager = ReturnType<typeof createInputManager>;
export function makeModifiersBitfield(e: MouseEvent | KeyboardEvent): number {
return Number(e.ctrlKey) | (Number(e.shiftKey) << 1) | (Number(e.altKey) << 2);
}