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