summaryrefslogtreecommitdiff
path: root/src/engine/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/engine/utils')
-rw-r--r--src/engine/utils/CodeEditor.ts200
-rw-r--r--src/engine/utils/Modal.ts45
-rw-r--r--src/engine/utils/index.ts4
-rw-r--r--src/engine/utils/modal.ts41
-rw-r--r--src/engine/utils/tryWrap.ts7
5 files changed, 255 insertions, 42 deletions
diff --git a/src/engine/utils/CodeEditor.ts b/src/engine/utils/CodeEditor.ts
new file mode 100644
index 0000000..e13d1d4
--- /dev/null
+++ b/src/engine/utils/CodeEditor.ts
@@ -0,0 +1,200 @@
+import {
+ EditorState,
+ StateField,
+ StateEffect,
+ Range,
+ Extension,
+} from "@codemirror/state";
+import { Decoration, EditorView, keymap } from "@codemirror/view";
+import { defaultKeymap } from "@codemirror/commands";
+import rainbowBrackets from "rainbowbrackets";
+import { basicSetup } from "codemirror";
+import { ModalInstance, ModalSingleton } from ".";
+import { EditorSave, Failure, ModalOpen, SOUNDS } from "../config";
+
+export interface CodeEditorError extends Error {
+ location: {
+ start: { offset: number };
+ end: { offset: number };
+ };
+}
+
+export type CodeConsumer = (code: string) => {
+ consumed?: boolean;
+ error?: CodeEditorError;
+};
+
+interface CodeEditorState {
+ view: EditorView;
+ editorElement: HTMLElement;
+ errorElement: HTMLElement;
+ resultElement: HTMLElement;
+ closeButton: HTMLButtonElement;
+ codeConsumer: CodeConsumer;
+}
+
+export class CodeEditorInstance {
+ constructor(
+ private modalInstance: ModalInstance = ModalSingleton,
+ private codeEditorState: CodeEditorState | null = null
+ ) {}
+
+ public close() {
+ if (!this.codeEditorState) {
+ return;
+ }
+
+ this.codeEditorState.view.destroy();
+ this.codeEditorState = null;
+
+ SOUNDS.get(EditorSave.name)!.play();
+ }
+
+ public open(
+ initCode: string,
+ codeConsumer: CodeEditorState["codeConsumer"],
+ readonly: boolean = false,
+ initResult: { data?: string; error?: string } = {},
+ ) {
+ if (this.codeEditorState) {
+ throw new Error("code editor instance is already owned.");
+ }
+ const modalContent = `
+ <div class='code'>
+ <div id='code'></div>
+ <br>
+ <p id='error' class='error'></p>
+ <p id='result' class='result'></p>
+ <button id='close-modal'>Done</button>
+ </div>
+ `;
+ this.modalInstance.open(modalContent);
+
+ const startState = EditorState.create({
+ doc: initCode,
+ extensions: [
+ basicSetup,
+ keymap.of(defaultKeymap),
+ rainbowBrackets(),
+ highlightExtension,
+ FontSizeThemeExtension,
+ EditorState.readOnly.of(readonly),
+ ],
+ });
+
+ const codeBox = document.getElementById("code")!;
+ const errorElement = document.getElementById("error")!;
+ const resultElement = document.getElementById("result")!;
+
+ const closeButton = document.getElementById(
+ "close-modal"
+ ) as HTMLButtonElement;
+ closeButton.addEventListener("click", () => this.onSave());
+
+ const editorView = new EditorView({
+ state: startState,
+ parent: codeBox,
+ });
+ editorView.focus();
+
+ this.codeEditorState = {
+ view: editorView,
+ editorElement: codeBox,
+ errorElement,
+ resultElement,
+ closeButton,
+ codeConsumer,
+ };
+ this.setResult(initResult);
+ }
+
+ private refreshCodeEditorText(text: string) {
+ if (!this.codeEditorState) {
+ return;
+ }
+ const { view } = this.codeEditorState;
+
+ view.dispatch({
+ changes: {
+ from: 0,
+ to: text.length,
+ insert: "",
+ },
+ });
+ view.dispatch({
+ changes: {
+ from: 0,
+ to: 0,
+ insert: text,
+ },
+ });
+ }
+
+ private onSave() {
+ if (!this.codeEditorState) {
+ throw new Error();
+ }
+
+ const { view } = this.codeEditorState;
+ const code = view.state.doc.toString();
+ this.refreshCodeEditorText(code);
+
+ const valid = this.codeEditorState.codeConsumer(code);
+ if (!valid.error) {
+ return;
+ }
+
+ const {
+ location: {
+ start: { offset: start },
+ end: { offset: end },
+ },
+ message,
+ } = valid.error;
+
+ view.dispatch({
+ effects: highlightEffect.of([
+ syntaxErrorDecoration.range(start === end ? start - 1 : start, end),
+ ]),
+ });
+
+ this.setResult({ error: message });
+ SOUNDS.get(Failure.name)!.play();
+ }
+
+ private setResult(result: { data?: string; error?: string }) {
+ if (!this.codeEditorState) return;
+ this.codeEditorState.resultElement.innerText = result.data ?? "";
+ this.codeEditorState.errorElement.innerText = result.error ?? "";
+ }
+}
+
+const highlightEffect = StateEffect.define<Range<Decoration>[]>();
+const highlightExtension = StateField.define({
+ create() {
+ return Decoration.none;
+ },
+ update(value, transaction) {
+ value = value.map(transaction.changes);
+
+ for (let effect of transaction.effects) {
+ if (effect.is(highlightEffect))
+ value = value.update({ add: effect.value, sort: true });
+ }
+
+ return value;
+ },
+ provide: (f) => EditorView.decorations.from(f),
+});
+
+const FontSizeTheme = EditorView.theme({
+ $: {
+ fontSize: "14pt",
+ },
+});
+const FontSizeThemeExtension: Extension = [FontSizeTheme];
+const syntaxErrorDecoration = Decoration.mark({
+ class: "syntax-error",
+});
+
+export const CodeEditorSingleton = new CodeEditorInstance();
diff --git a/src/engine/utils/Modal.ts b/src/engine/utils/Modal.ts
new file mode 100644
index 0000000..d46cade
--- /dev/null
+++ b/src/engine/utils/Modal.ts
@@ -0,0 +1,45 @@
+import { Miscellaneous } from "../config";
+
+export class ModalInstance {
+ private modalOpen: boolean = false;
+ private elementId: string = Miscellaneous.MODAL_ID;
+ private contentId: string = Miscellaneous.MODAL_CONTENT_ID;
+
+ public open(content: string) {
+ const modal = document.getElementById(this.elementId);
+ const modalContent = document.getElementById(this.contentId);
+ if (!modal || this.modalOpen) {
+ return;
+ }
+
+ this.modalOpen = true;
+ modal.style.display = "flex";
+ modal.style.animation = "fadeIn 0.25s";
+
+ modalContent!.innerHTML = content;
+ modalContent!.style.animation = "scaleUp 0.25s";
+ }
+
+ public vanish(): Promise<void> {
+ const modal = document.getElementById(this.elementId);
+ const modalContent = document.getElementById(this.contentId);
+ return new Promise((res, _rej) => {
+ if (!(modal && this.modalOpen && modalContent)) {
+ res();
+ return;
+ }
+ modal.style.animation = "fadeOut 0.25s";
+ modalContent.style.animation = "scaleDown 0.25s";
+
+ setTimeout(() => {
+ modalContent.innerHTML = "";
+ modal.style.display = "none";
+
+ this.modalOpen = false;
+ res();
+ }, 200);
+ });
+ }
+}
+
+export const ModalSingleton = new ModalInstance(); \ No newline at end of file
diff --git a/src/engine/utils/index.ts b/src/engine/utils/index.ts
index 48b94b8..ffeef7a 100644
--- a/src/engine/utils/index.ts
+++ b/src/engine/utils/index.ts
@@ -1,6 +1,8 @@
export * from "./clamp";
export * from "./dotProduct";
export * from "./rotateVector";
-export * from "./modal";
export * from "./colors";
export * from "./random";
+export * from './tryWrap';
+export * from "./Modal";
+export * from "./CodeEditor"; \ No newline at end of file
diff --git a/src/engine/utils/modal.ts b/src/engine/utils/modal.ts
deleted file mode 100644
index a378821..0000000
--- a/src/engine/utils/modal.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Miscellaneous } from "../config";
-
-let modalOpen = false;
-
-export const openModal = (
- content: string,
- id = Miscellaneous.MODAL_ID,
- contentId = Miscellaneous.MODAL_CONTENT_ID,
-) => {
- const modal = document.getElementById(id);
- const modalContent = document.getElementById(contentId);
- if (modal && !modalOpen && modalContent) {
- modal.style.display = "flex";
- modal.style.animation = "fadeIn 0.25s";
-
- modalContent.innerHTML = content;
- modalContent.style.animation = "scaleUp 0.25s";
-
- modalOpen = true;
- }
-};
-
-export const closeModal = (
- id = Miscellaneous.MODAL_ID,
- contentId = Miscellaneous.MODAL_CONTENT_ID,
-) => {
- const modal = document.getElementById(id);
- const modalContent = document.getElementById(contentId);
-
- if (modal && modalOpen && modalContent) {
- modal.style.animation = "fadeOut 0.25s";
- modalContent.style.animation = "scaleDown 0.25s";
-
- setTimeout(() => {
- modalContent.innerHTML = "";
- modal.style.display = "none";
-
- modalOpen = false;
- }, 200);
- }
-};
diff --git a/src/engine/utils/tryWrap.ts b/src/engine/utils/tryWrap.ts
new file mode 100644
index 0000000..12e23ea
--- /dev/null
+++ b/src/engine/utils/tryWrap.ts
@@ -0,0 +1,7 @@
+export const tryWrap = <T>(supplier: () => T): { data?: T; error?: any } => {
+ try {
+ return { data: supplier() };
+ } catch (error) {
+ return { error: error as any };
+ }
+};