Skip to content
Snippets Groups Projects
Verified Commit 2bb750da authored by João Magalhães's avatar João Magalhães :rocket:
Browse files

feat: added toast support

parent 4f684215
No related branches found
No related tags found
1 merge request!9Version 0.4.0 🍾
Pipeline #1384 passed
...@@ -111,64 +111,6 @@ p { ...@@ -111,64 +111,6 @@ p {
display: block; 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 { .button-area {
user-select: none; user-select: none;
-o-user-select: none; -o-user-select: none;
......
...@@ -118,9 +118,6 @@ ...@@ -118,9 +118,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="toast-container">
<div id="toast" class="toast"></div>
</div>
</body> </body>
<div id="overlay" class="overlay"> <div id="overlay" class="overlay">
<div class="overlay-container"> <div class="overlay-container">
......
...@@ -77,7 +77,6 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -77,7 +77,6 @@ class GameboyEmulator extends Observable implements Emulator {
private visualFrequency: number = VISUAL_HZ; private visualFrequency: number = VISUAL_HZ;
private idleFrequency: number = IDLE_HZ; private idleFrequency: number = IDLE_HZ;
private toastTimeout: number | null = null;
private paused: boolean = false; private paused: boolean = false;
private nextTickTime: number = 0; private nextTickTime: number = 0;
private fps: number = 0; private fps: number = 0;
...@@ -96,7 +95,6 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -96,7 +95,6 @@ class GameboyEmulator extends Observable implements Emulator {
// initializes the complete set of sub-systems // initializes the complete set of sub-systems
// and registers the event handlers // and registers the event handlers
await this.init();
await this.register(); await this.register();
// boots the emulator subsystem with the initial // boots the emulator subsystem with the initial
...@@ -143,7 +141,11 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -143,7 +141,11 @@ class GameboyEmulator extends Observable implements Emulator {
// displays the error information to both the end-user // displays the error information to both the end-user
// and the developer (for diagnostics) // and the developer (for diagnostics)
this.showToast(message, true, 5000); this.trigger("message", {
text: message,
error: true,
timeout: 5000
});
console.error(err); console.error(err);
// pauses the machine, allowing the end-user to act // pauses the machine, allowing the end-user to act
...@@ -211,8 +213,8 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -211,8 +213,8 @@ class GameboyEmulator extends Observable implements Emulator {
// frame is different from the previously rendered // frame is different from the previously rendered
// one then it's time to update the canvas // one then it's time to update the canvas
if ( if (
this.gameBoy!.ppu_mode() == PpuMode.VBlank && this.gameBoy!.ppu_mode() === PpuMode.VBlank &&
this.gameBoy!.ppu_frame() != lastFrame this.gameBoy!.ppu_frame() !== lastFrame
) { ) {
lastFrame = this.gameBoy!.ppu_frame(); lastFrame = this.gameBoy!.ppu_frame();
...@@ -233,7 +235,7 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -233,7 +235,7 @@ class GameboyEmulator extends Observable implements Emulator {
const currentTime = new Date().getTime(); const currentTime = new Date().getTime();
const deltaTime = (currentTime - this.frameStart) / 1000; const deltaTime = (currentTime - this.frameStart) / 1000;
const fps = Math.round(this.frameCount / deltaTime); const fps = Math.round(this.frameCount / deltaTime);
this.setFps(fps); this.fps = fps;
this.frameCount = 0; this.frameCount = 0;
this.frameStart = currentTime; this.frameStart = currentTime;
} }
...@@ -310,10 +312,7 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -310,10 +312,7 @@ class GameboyEmulator extends Observable implements Emulator {
// updates the complete set of global information that // updates the complete set of global information that
// is going to be displayed // is going to be displayed
this.setEngine(this.engine!);
this.setRom(romName!, romData!, cartridge); this.setRom(romName!, romData!, cartridge);
this.setLogicFrequency(this.logicFrequency);
this.setFps(this.fps);
// in case the restore (state) flag is set // in case the restore (state) flag is set
// then resumes the machine execution // then resumes the machine execution
...@@ -330,15 +329,10 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -330,15 +329,10 @@ class GameboyEmulator extends Observable implements Emulator {
this.registerDrop(), this.registerDrop(),
this.registerKeys(), this.registerKeys(),
this.registerButtons(), this.registerButtons(),
this.registerKeyboard(), this.registerKeyboard()
this.registerToast()
]); ]);
} }
async init() {
await Promise.all([this.initBase()]);
}
registerDrop() { registerDrop() {
document.addEventListener("drop", async (event) => { document.addEventListener("drop", async (event) => {
if ( if (
...@@ -357,10 +351,10 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -357,10 +351,10 @@ class GameboyEmulator extends Observable implements Emulator {
const file = event.dataTransfer!.files[0]; const file = event.dataTransfer!.files[0];
if (!file.name.endsWith(".gb")) { if (!file.name.endsWith(".gb")) {
this.showToast( this.trigger("message", {
"This is probably not a Game Boy ROM file!", text: "This is probably not a Game Boy ROM file!",
true error: true
); });
return; return;
} }
...@@ -369,7 +363,9 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -369,7 +363,9 @@ class GameboyEmulator extends Observable implements Emulator {
this.boot({ engine: null, romName: file.name, romData: romData }); 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) => { document.addEventListener("dragover", async (event) => {
if (!event.dataTransfer!.items || event.dataTransfer!.items[0].type) if (!event.dataTransfer!.items || event.dataTransfer!.items[0].type)
...@@ -406,15 +402,11 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -406,15 +402,11 @@ class GameboyEmulator extends Observable implements Emulator {
switch (event.key) { switch (event.key) {
case "+": case "+":
this.setLogicFrequency( this.logicFrequency += FREQUENCY_DELTA;
this.logicFrequency + FREQUENCY_DELTA
);
break; break;
case "-": case "-":
this.setLogicFrequency( this.logicFrequency -= FREQUENCY_DELTA;
this.logicFrequency - FREQUENCY_DELTA
);
break; break;
} }
}); });
...@@ -433,25 +425,25 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -433,25 +425,25 @@ class GameboyEmulator extends Observable implements Emulator {
registerButtons() { registerButtons() {
const engine = document.getElementById("engine")!; const engine = document.getElementById("engine")!;
engine.addEventListener("click", () => { engine.addEventListener("click", () => {
const name = this.engine == "neo" ? "classic" : "neo"; const name = this.engine === "neo" ? "classic" : "neo";
this.boot({ engine: name }); this.boot({ engine: name });
this.showToast( this.trigger("message", {
`Game Boy running in engine "${name.toUpperCase()}" from now on!` text: `Game Boy running in engine "${name.toUpperCase()}" from now on!`
); });
}); });
const logicFrequencyPlus = document.getElementById( const logicFrequencyPlus = document.getElementById(
"logic-frequency-plus" "logic-frequency-plus"
)!; )!;
logicFrequencyPlus.addEventListener("click", () => { logicFrequencyPlus.addEventListener("click", () => {
this.setLogicFrequency(this.logicFrequency + FREQUENCY_DELTA); this.logicFrequency = this.logicFrequency + FREQUENCY_DELTA;
}); });
const logicFrequencyMinus = document.getElementById( const logicFrequencyMinus = document.getElementById(
"logic-frequency-minus" "logic-frequency-minus"
)!; )!;
logicFrequencyMinus.addEventListener("click", () => { logicFrequencyMinus.addEventListener("click", () => {
this.setLogicFrequency(this.logicFrequency - FREQUENCY_DELTA); this.logicFrequency = this.logicFrequency - FREQUENCY_DELTA;
}); });
const buttonPause = document.getElementById("button-pause")!; const buttonPause = document.getElementById("button-pause")!;
...@@ -476,15 +468,14 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -476,15 +468,14 @@ class GameboyEmulator extends Observable implements Emulator {
} }
const delta = (Date.now() - initial) / 1000; const delta = (Date.now() - initial) / 1000;
const frequency_mhz = count / delta / 1000 / 1000; const frequency_mhz = count / delta / 1000 / 1000;
this.showToast( this.trigger("message", {
`Took ${delta.toFixed( text: `Took ${delta.toFixed(
2 2
)} seconds to run ${count} ticks (${frequency_mhz.toFixed( )} seconds to run ${count} ticks (${frequency_mhz.toFixed(
2 2
)} Mhz)!`, )} Mhz)!`,
undefined, timeout: 7500
7500 });
);
} finally { } finally {
this.resume(); this.resume();
buttonBenchmark.classList.remove("enabled"); buttonBenchmark.classList.remove("enabled");
...@@ -580,13 +571,13 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -580,13 +571,13 @@ class GameboyEmulator extends Observable implements Emulator {
(pixels[index] << 24) | (pixels[index] << 24) |
(pixels[index + 1] << 16) | (pixels[index + 1] << 16) |
(pixels[index + 2] << 8) | (pixels[index + 2] << 8) |
(format == PixelFormat.RGBA (format === PixelFormat.RGBA
? pixels[index + 3] ? pixels[index + 3]
: 0xff); : 0xff);
buffer.setUint32(offset, color); buffer.setUint32(offset, color);
counter++; counter++;
if (counter == 8) { if (counter === 8) {
counter = 0; counter = 0;
offset += offset +=
(canvasTiles.width - 7) * PixelFormat.RGBA; (canvasTiles.width - 7) * PixelFormat.RGBA;
...@@ -653,7 +644,9 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -653,7 +644,9 @@ class GameboyEmulator extends Observable implements Emulator {
this.boot({ engine: null, romName: file.name, romData: romData }); 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 { ...@@ -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) { setRom(name: string, data: Uint8Array, cartridge: Cartridge) {
this.romName = name; this.romName = name;
this.romData = data; this.romData = data;
...@@ -749,19 +709,6 @@ class GameboyEmulator extends Observable implements Emulator { ...@@ -749,19 +709,6 @@ class GameboyEmulator extends Observable implements Emulator {
this.cartridge = cartridge; 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() { getName() {
return "Boytacean"; return "Boytacean";
} }
......
...@@ -19,12 +19,13 @@ import { ...@@ -19,12 +19,13 @@ import {
PanelSplit, PanelSplit,
Paragraph, Paragraph,
Section, Section,
Title Title,
Toast
} from "./components"; } from "./components";
import "./app.css"; 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 * Abstract class that implements the basic functionality
...@@ -42,9 +43,9 @@ export class Observable { ...@@ -42,9 +43,9 @@ export class Observable {
this.events[event] = callbacks; this.events[event] = callbacks;
} }
trigger(event: string) { trigger(event: string, params?: Record<string, any>) {
const callbacks = this.events[event] ?? []; 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"] }) => { ...@@ -167,10 +168,14 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
const [romInfo, setRomInfo] = useState<RomInfo>({}); const [romInfo, setRomInfo] = useState<RomInfo>({});
const [framerate, setFramerate] = useState(0); const [framerate, setFramerate] = useState(0);
const [keyaction, setKeyaction] = useState<string>(); const [keyaction, setKeyaction] = useState<string>();
const [modalVisible, setModalVisible] = useState(false);
const [modalTitle, setModalTitle] = useState<string>(); const [modalTitle, setModalTitle] = useState<string>();
const [modalText, setModalText] = 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 frameRef = useRef<boolean>(false);
const errorRef = useRef<boolean>(false); const errorRef = useRef<boolean>(false);
const modalCallbackRef = const modalCallbackRef =
...@@ -208,6 +213,9 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { ...@@ -208,6 +213,9 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
const romInfo = emulator.getRomInfo(); const romInfo = emulator.getRomInfo();
setRomInfo(romInfo); setRomInfo(romInfo);
}); });
emulator.bind("message", (_, params = {}) => {
showToast(params.text, params.error, params.timeout);
});
}, []); }, []);
const getPauseText = () => (paused ? "Resume" : "Pause"); const getPauseText = () => (paused ? "Resume" : "Pause");
...@@ -227,6 +235,20 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { ...@@ -227,6 +235,20 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
})) as boolean; })) as boolean;
return result; 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 = () => { const onModalConfirm = () => {
if (modalCallbackRef.current) { if (modalCallbackRef.current) {
...@@ -242,6 +264,9 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { ...@@ -242,6 +264,9 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
} }
setModalVisible(false); setModalVisible(false);
}; };
const onToastCancel = () => {
setToastVisible(false);
};
const onPauseClick = () => { const onPauseClick = () => {
emulator.toggleRunning(); emulator.toggleRunning();
setPaused(!paused); setPaused(!paused);
...@@ -254,7 +279,12 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { ...@@ -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!", "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!",
"Confirm" "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 = () => { const onFullscreenClick = () => {
setFullscreen(!fullscreen); setFullscreen(!fullscreen);
...@@ -284,12 +314,18 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { ...@@ -284,12 +314,18 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
return ( return (
<div className="app"> <div className="app">
<Modal <Modal
visible={modalVisible}
title={modalTitle} title={modalTitle}
text={modalText} text={modalText}
visible={modalVisible}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}
onCancel={onModalCancel} onCancel={onModalCancel}
/> />
<Toast
text={toastText}
error={toastError}
visible={toastVisible}
onCancel={onToastCancel}
/>
<Footer color={getBackground()}> <Footer color={getBackground()}>
Built with ❤️ by{" "} Built with ❤️ by{" "}
<Link href="https://joao.me" target="_blank"> <Link href="https://joao.me" target="_blank">
......
.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;
}
...@@ -3,12 +3,34 @@ import React, { FC } from "react"; ...@@ -3,12 +3,34 @@ import React, { FC } from "react";
import "./toast.css"; import "./toast.css";
type ToastProps = { type ToastProps = {
text?: string;
error?: boolean;
visible?: boolean;
style?: string[]; style?: string[];
onCancel?: () => void;
}; };
export const Toast: FC<ToastProps> = ({ style = [] }) => { export const Toast: FC<ToastProps> = ({
const classes = () => ["toast", ...style].join(" "); text = "",
return <div className={classes()}></div>; 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; export default Toast;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment