From 2bb750dae277f43562a97b2973b190fd05d7c9d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com>
Date: Sun, 30 Oct 2022 19:53:09 +0000
Subject: [PATCH] feat: added toast support

---
 examples/web/index.css                        |  58 ---------
 examples/web/index.html                       |   3 -
 examples/web/index.ts                         | 119 +++++-------------
 examples/web/react/app.tsx                    |  50 ++++++--
 examples/web/react/components/toast/toast.css |  57 +++++++++
 examples/web/react/components/toast/toast.tsx |  28 ++++-
 6 files changed, 158 insertions(+), 157 deletions(-)

diff --git a/examples/web/index.css b/examples/web/index.css
index 35b667a0..a71f9876 100644
--- a/examples/web/index.css
+++ b/examples/web/index.css
@@ -111,64 +111,6 @@ p {
     display: block;
 }
 
-.toast-container {
-    background-color: black;
-    height: 0px;
-    left: 0px;
-    padding: 0px 24px 0px 24px;
-    pointer-events: none;
-    position: fixed;
-    text-align: center;
-    top: 0px;
-    width: 100%;
-    z-index: 8;
-}
-
-.toast-container > .toast {
-    background-color: #2a9d8f;
-    border-radius: 4px 4px 4px 4px;
-    -o-border-radius: 4px 4px 4px 4px;
-    -ms-border-radius: 4px 4px 4px 4px;
-    -moz-border-radius: 4px 4px 4px 4px;
-    -khtml-border-radius: 4px 4px 4px 4px;
-    -webkit-border-radius: 4px 4px 4px 4px;
-    cursor: pointer;
-    display: inline-block;
-    font-size: 20px;
-    line-height: 22px;
-    opacity: 0.0;
-    -o-opacity: 0.0;
-    -ms-opacity: 0.0;
-    -moz-opacity: 0.0;
-    -khtml-opacity: 0.0;
-    -webkit-opacity: 0.0;
-    padding: 12px 18px 12px 18px;
-    position: relative;
-    top: -46px;
-    transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -o-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -ms-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -moz-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -khtml-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -webkit-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    width: fit-content;
-}
-
-.toast-container > .toast.error {
-    background-color: #e63946;
-}
-
-.toast-container > .toast.visible {
-    opacity: 1.0;
-    -o-opacity: 1.0;
-    -ms-opacity: 1.0;
-    -moz-opacity: 1.0;
-    -khtml-opacity: 1.0;
-    -webkit-opacity: 1.0;
-    pointer-events: all;
-    top: 24px;
-}
-
 .button-area {
     user-select: none;
     -o-user-select: none;
diff --git a/examples/web/index.html b/examples/web/index.html
index a0577917..192d377a 100644
--- a/examples/web/index.html
+++ b/examples/web/index.html
@@ -118,9 +118,6 @@
             </div>
         </div>
     </div>
-    <div class="toast-container">
-        <div id="toast" class="toast"></div>
-    </div>
 </body>
 <div id="overlay" class="overlay">
     <div class="overlay-container">
diff --git a/examples/web/index.ts b/examples/web/index.ts
index e8e3ff62..579d30c8 100644
--- a/examples/web/index.ts
+++ b/examples/web/index.ts
@@ -77,7 +77,6 @@ class GameboyEmulator extends Observable implements Emulator {
     private visualFrequency: number = VISUAL_HZ;
     private idleFrequency: number = IDLE_HZ;
 
-    private toastTimeout: number | null = null;
     private paused: boolean = false;
     private nextTickTime: number = 0;
     private fps: number = 0;
@@ -96,7 +95,6 @@ class GameboyEmulator extends Observable implements Emulator {
 
         // initializes the complete set of sub-systems
         // and registers the event handlers
-        await this.init();
         await this.register();
 
         // boots the emulator subsystem with the initial
@@ -143,7 +141,11 @@ class GameboyEmulator extends Observable implements Emulator {
 
                 // displays the error information to both the end-user
                 // and the developer (for diagnostics)
-                this.showToast(message, true, 5000);
+                this.trigger("message", {
+                    text: message,
+                    error: true,
+                    timeout: 5000
+                });
                 console.error(err);
 
                 // pauses the machine, allowing the end-user to act
@@ -211,8 +213,8 @@ class GameboyEmulator extends Observable implements Emulator {
             // frame is different from the previously rendered
             // one then it's time to update the canvas
             if (
-                this.gameBoy!.ppu_mode() == PpuMode.VBlank &&
-                this.gameBoy!.ppu_frame() != lastFrame
+                this.gameBoy!.ppu_mode() === PpuMode.VBlank &&
+                this.gameBoy!.ppu_frame() !== lastFrame
             ) {
                 lastFrame = this.gameBoy!.ppu_frame();
 
@@ -233,7 +235,7 @@ class GameboyEmulator extends Observable implements Emulator {
             const currentTime = new Date().getTime();
             const deltaTime = (currentTime - this.frameStart) / 1000;
             const fps = Math.round(this.frameCount / deltaTime);
-            this.setFps(fps);
+            this.fps = fps;
             this.frameCount = 0;
             this.frameStart = currentTime;
         }
@@ -310,10 +312,7 @@ class GameboyEmulator extends Observable implements Emulator {
 
         // updates the complete set of global information that
         // is going to be displayed
-        this.setEngine(this.engine!);
         this.setRom(romName!, romData!, cartridge);
-        this.setLogicFrequency(this.logicFrequency);
-        this.setFps(this.fps);
 
         // in case the restore (state) flag is set
         // then resumes the machine execution
@@ -330,15 +329,10 @@ class GameboyEmulator extends Observable implements Emulator {
             this.registerDrop(),
             this.registerKeys(),
             this.registerButtons(),
-            this.registerKeyboard(),
-            this.registerToast()
+            this.registerKeyboard()
         ]);
     }
 
-    async init() {
-        await Promise.all([this.initBase()]);
-    }
-
     registerDrop() {
         document.addEventListener("drop", async (event) => {
             if (
@@ -357,10 +351,10 @@ class GameboyEmulator extends Observable implements Emulator {
             const file = event.dataTransfer!.files[0];
 
             if (!file.name.endsWith(".gb")) {
-                this.showToast(
-                    "This is probably not a Game Boy ROM file!",
-                    true
-                );
+                this.trigger("message", {
+                    text: "This is probably not a Game Boy ROM file!",
+                    error: true
+                });
                 return;
             }
 
@@ -369,7 +363,9 @@ class GameboyEmulator extends Observable implements Emulator {
 
             this.boot({ engine: null, romName: file.name, romData: romData });
 
-            this.showToast(`Loaded ${file.name} ROM successfully!`);
+            this.trigger("message", {
+                text: `Loaded ${file.name} ROM successfully!`
+            });
         });
         document.addEventListener("dragover", async (event) => {
             if (!event.dataTransfer!.items || event.dataTransfer!.items[0].type)
@@ -406,15 +402,11 @@ class GameboyEmulator extends Observable implements Emulator {
 
             switch (event.key) {
                 case "+":
-                    this.setLogicFrequency(
-                        this.logicFrequency + FREQUENCY_DELTA
-                    );
+                    this.logicFrequency += FREQUENCY_DELTA;
                     break;
 
                 case "-":
-                    this.setLogicFrequency(
-                        this.logicFrequency - FREQUENCY_DELTA
-                    );
+                    this.logicFrequency -= FREQUENCY_DELTA;
                     break;
             }
         });
@@ -433,25 +425,25 @@ class GameboyEmulator extends Observable implements Emulator {
     registerButtons() {
         const engine = document.getElementById("engine")!;
         engine.addEventListener("click", () => {
-            const name = this.engine == "neo" ? "classic" : "neo";
+            const name = this.engine === "neo" ? "classic" : "neo";
             this.boot({ engine: name });
-            this.showToast(
-                `Game Boy running in engine "${name.toUpperCase()}" from now on!`
-            );
+            this.trigger("message", {
+                text: `Game Boy running in engine "${name.toUpperCase()}" from now on!`
+            });
         });
 
         const logicFrequencyPlus = document.getElementById(
             "logic-frequency-plus"
         )!;
         logicFrequencyPlus.addEventListener("click", () => {
-            this.setLogicFrequency(this.logicFrequency + FREQUENCY_DELTA);
+            this.logicFrequency = this.logicFrequency + FREQUENCY_DELTA;
         });
 
         const logicFrequencyMinus = document.getElementById(
             "logic-frequency-minus"
         )!;
         logicFrequencyMinus.addEventListener("click", () => {
-            this.setLogicFrequency(this.logicFrequency - FREQUENCY_DELTA);
+            this.logicFrequency = this.logicFrequency - FREQUENCY_DELTA;
         });
 
         const buttonPause = document.getElementById("button-pause")!;
@@ -476,15 +468,14 @@ class GameboyEmulator extends Observable implements Emulator {
                 }
                 const delta = (Date.now() - initial) / 1000;
                 const frequency_mhz = count / delta / 1000 / 1000;
-                this.showToast(
-                    `Took ${delta.toFixed(
+                this.trigger("message", {
+                    text: `Took ${delta.toFixed(
                         2
                     )} seconds to run ${count} ticks (${frequency_mhz.toFixed(
                         2
                     )} Mhz)!`,
-                    undefined,
-                    7500
-                );
+                    timeout: 7500
+                });
             } finally {
                 this.resume();
                 buttonBenchmark.classList.remove("enabled");
@@ -580,13 +571,13 @@ class GameboyEmulator extends Observable implements Emulator {
                             (pixels[index] << 24) |
                             (pixels[index + 1] << 16) |
                             (pixels[index + 2] << 8) |
-                            (format == PixelFormat.RGBA
+                            (format === PixelFormat.RGBA
                                 ? pixels[index + 3]
                                 : 0xff);
                         buffer.setUint32(offset, color);
 
                         counter++;
-                        if (counter == 8) {
+                        if (counter === 8) {
                             counter = 0;
                             offset +=
                                 (canvasTiles.width - 7) * PixelFormat.RGBA;
@@ -653,7 +644,9 @@ class GameboyEmulator extends Observable implements Emulator {
 
             this.boot({ engine: null, romName: file.name, romData: romData });
 
-            this.showToast(`Loaded ${file.name} ROM successfully!`);
+            this.trigger("message", {
+                text: `Loaded ${file.name} ROM successfully!`
+            });
         });
     }
 
@@ -709,39 +702,6 @@ class GameboyEmulator extends Observable implements Emulator {
         });
     }
 
-    registerToast() {
-        const toast = document.getElementById("toast")!;
-        toast.addEventListener("click", () => {
-            toast.classList.remove("visible");
-        });
-    }
-
-    async initBase() {
-        this.setVersion(info.version);
-    }
-
-    async showToast(message: string, error = false, timeout = 3500) {
-        const toast = document.getElementById("toast")!;
-        toast.classList.remove("error");
-        if (error) toast.classList.add("error");
-        toast.classList.add("visible");
-        toast.textContent = message;
-        if (this.toastTimeout) clearTimeout(this.toastTimeout);
-        this.toastTimeout = setTimeout(() => {
-            toast.classList.remove("visible");
-            this.toastTimeout = null;
-        }, timeout);
-    }
-
-    setVersion(value: string) {
-        document.getElementById("version")!.textContent = value;
-    }
-
-    setEngine(name: string, upper = true) {
-        name = upper ? name.toUpperCase() : name;
-        document.getElementById("engine")!.textContent = name;
-    }
-
     setRom(name: string, data: Uint8Array, cartridge: Cartridge) {
         this.romName = name;
         this.romData = data;
@@ -749,19 +709,6 @@ class GameboyEmulator extends Observable implements Emulator {
         this.cartridge = cartridge;
     }
 
-    setLogicFrequency(value: number) {
-        if (value < 0) this.showToast("Invalid frequency value!", true);
-        value = Math.max(value, 0);
-        this.logicFrequency = value;
-        document.getElementById("logic-frequency")!.textContent = String(value);
-    }
-
-    setFps(value: number) {
-        if (value < 0) this.showToast("Invalid FPS value!", true);
-        value = Math.max(value, 0);
-        this.fps = value;
-    }
-
     getName() {
         return "Boytacean";
     }
diff --git a/examples/web/react/app.tsx b/examples/web/react/app.tsx
index 6cc0fba4..45e7b722 100644
--- a/examples/web/react/app.tsx
+++ b/examples/web/react/app.tsx
@@ -19,12 +19,13 @@ import {
     PanelSplit,
     Paragraph,
     Section,
-    Title
+    Title,
+    Toast
 } from "./components";
 
 import "./app.css";
 
-export type Callback<T> = (owner: T) => void;
+export type Callback<T> = (owner: T, params?: Record<string, any>) => void;
 
 /**
  * Abstract class that implements the basic functionality
@@ -42,9 +43,9 @@ export class Observable {
         this.events[event] = callbacks;
     }
 
-    trigger(event: string) {
+    trigger(event: string, params?: Record<string, any>) {
         const callbacks = this.events[event] ?? [];
-        callbacks.forEach((c) => c(this));
+        callbacks.forEach((c) => c(this, params));
     }
 }
 
@@ -167,10 +168,14 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
     const [romInfo, setRomInfo] = useState<RomInfo>({});
     const [framerate, setFramerate] = useState(0);
     const [keyaction, setKeyaction] = useState<string>();
-    const [modalVisible, setModalVisible] = useState(false);
     const [modalTitle, setModalTitle] = useState<string>();
     const [modalText, setModalText] = useState<string>();
+    const [modalVisible, setModalVisible] = useState(false);
+    const [toastText, setToastText] = useState<string>();
+    const [toastError, setToastError] = useState(false);
+    const [toastVisible, setToastVisible] = useState(false);
 
+    const toastCounterRef = useRef(0);
     const frameRef = useRef<boolean>(false);
     const errorRef = useRef<boolean>(false);
     const modalCallbackRef =
@@ -208,6 +213,9 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
             const romInfo = emulator.getRomInfo();
             setRomInfo(romInfo);
         });
+        emulator.bind("message", (_, params = {}) => {
+            showToast(params.text, params.error, params.timeout);
+        });
     }, []);
 
     const getPauseText = () => (paused ? "Resume" : "Pause");
@@ -227,6 +235,20 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
         })) as boolean;
         return result;
     };
+    const showToast = async (text: string, error = false, timeout = 3500) => {
+        setToastText(text);
+        setToastError(error);
+        setToastVisible(true);
+        toastCounterRef.current++;
+        const counter = toastCounterRef.current;
+        await new Promise((resolve) => {
+            setTimeout(() => {
+                if (counter !== toastCounterRef.current) return;
+                setToastVisible(false);
+                resolve(true);
+            }, timeout);
+        });
+    };
 
     const onModalConfirm = () => {
         if (modalCallbackRef.current) {
@@ -242,6 +264,9 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
         }
         setModalVisible(false);
     };
+    const onToastCancel = () => {
+        setToastVisible(false);
+    };
     const onPauseClick = () => {
         emulator.toggleRunning();
         setPaused(!paused);
@@ -254,7 +279,12 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
             "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!",
             "Confirm"
         );
-        alert(`Will run it as ${result}`);
+        await showToast(
+            result
+                ? "Will run the benchmark as fast as possible"
+                : "Will not run the benchmark",
+            !result
+        );
     };
     const onFullscreenClick = () => {
         setFullscreen(!fullscreen);
@@ -284,12 +314,18 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
     return (
         <div className="app">
             <Modal
-                visible={modalVisible}
                 title={modalTitle}
                 text={modalText}
+                visible={modalVisible}
                 onConfirm={onModalConfirm}
                 onCancel={onModalCancel}
             />
+            <Toast
+                text={toastText}
+                error={toastError}
+                visible={toastVisible}
+                onCancel={onToastCancel}
+            />
             <Footer color={getBackground()}>
                 Built with ❤️ by{" "}
                 <Link href="https://joao.me" target="_blank">
diff --git a/examples/web/react/components/toast/toast.css b/examples/web/react/components/toast/toast.css
index e69de29b..8bf52332 100644
--- a/examples/web/react/components/toast/toast.css
+++ b/examples/web/react/components/toast/toast.css
@@ -0,0 +1,57 @@
+.toast {
+    background-color: black;
+    height: 0px;
+    left: 0px;
+    padding: 0px 24px 0px 24px;
+    pointer-events: none;
+    position: fixed;
+    text-align: center;
+    top: 0px;
+    width: 100%;
+    z-index: 8;
+}
+
+.toast > .toast-text {
+    background-color: #2a9d8f;
+    border-radius: 4px 4px 4px 4px;
+    -o-border-radius: 4px 4px 4px 4px;
+    -ms-border-radius: 4px 4px 4px 4px;
+    -moz-border-radius: 4px 4px 4px 4px;
+    -khtml-border-radius: 4px 4px 4px 4px;
+    -webkit-border-radius: 4px 4px 4px 4px;
+    cursor: pointer;
+    display: inline-block;
+    font-size: 20px;
+    line-height: 22px;
+    opacity: 0.0;
+    -o-opacity: 0.0;
+    -ms-opacity: 0.0;
+    -moz-opacity: 0.0;
+    -khtml-opacity: 0.0;
+    -webkit-opacity: 0.0;
+    padding: 12px 18px 12px 18px;
+    position: relative;
+    top: -46px;
+    transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    width: fit-content;
+}
+
+.toast.error > .toast-text {
+    background-color: #e63946;
+}
+
+.toast.visible > .toast-text {
+    opacity: 1.0;
+    -o-opacity: 1.0;
+    -ms-opacity: 1.0;
+    -moz-opacity: 1.0;
+    -khtml-opacity: 1.0;
+    -webkit-opacity: 1.0;
+    pointer-events: all;
+    top: 24px;
+}
diff --git a/examples/web/react/components/toast/toast.tsx b/examples/web/react/components/toast/toast.tsx
index 5f0efbe8..3c1f3831 100644
--- a/examples/web/react/components/toast/toast.tsx
+++ b/examples/web/react/components/toast/toast.tsx
@@ -3,12 +3,34 @@ import React, { FC } from "react";
 import "./toast.css";
 
 type ToastProps = {
+    text?: string;
+    error?: boolean;
+    visible?: boolean;
     style?: string[];
+    onCancel?: () => void;
 };
 
-export const Toast: FC<ToastProps> = ({ style = [] }) => {
-    const classes = () => ["toast", ...style].join(" ");
-    return <div className={classes()}></div>;
+export const Toast: FC<ToastProps> = ({
+    text = "",
+    error = false,
+    visible = false,
+    style = [],
+    onCancel
+}) => {
+    const classes = () =>
+        [
+            "toast",
+            error ? "error" : "",
+            visible ? "visible" : "",
+            ...style
+        ].join(" ");
+    return (
+        <div className={classes()}>
+            <div className="toast-text" onClick={onCancel}>
+                {text}
+            </div>
+        </div>
+    );
 };
 
 export default Toast;
-- 
GitLab