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 {
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;
......
......@@ -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">
......
......@@ -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";
}
......
......@@ -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">
......
.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";
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;
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