Skip to content
Snippets Groups Projects
app.tsx 13 KiB
Newer Older
  • Learn to ignore specific revisions
  • João Magalhães's avatar
    João Magalhães committed
    import React, { FC, useEffect, useRef, useState } from "react";
    
    import ReactDOM from "react-dom/client";
    
    
    declare const require: any;
    
    
        ButtonIncrement,
        ButtonSwitch,
    
        ClearHandler,
    
        Display,
    
        Footer,
    
        PanelSplit,
    
        Paragraph,
    
        Section,
        Title
    
    } from "./components";
    
    
    import "./app.css";
    
    
    export type Callback<T> = (owner: T) => void;
    
    /**
     * Abstract class that implements the basic functionality
    
     * part of the definition of the Observer pattern.
    
     *
     * @see {@link https://en.wikipedia.org/wiki/Observer_pattern}
     */
    export class Observable {
        private events: Record<string, [Callback<this>]> = {};
    
        bind(event: string, callback: Callback<this>) {
            const callbacks = this.events[event] ?? [];
            if (callbacks.includes(callback)) return;
            callbacks.push(callback);
            this.events[event] = callbacks;
        }
    
        trigger(event: string) {
            const callbacks = this.events[event] ?? [];
            callbacks.forEach((c) => c(this));
        }
    }
    
    
    export type RomInfo = {
        name?: string;
        data?: Uint8Array;
        size?: number;
        extra?: Record<string, string | undefined>;
    };
    
    export interface ObservableI {
        bind(event: string, callback: Callback<this>): void;
        trigger(event: string): void;
    }
    
    
    /**
     * Top level interface that declares the main abstract
     * interface of an emulator structured entity.
     * Should allow typical hardware operations to be performed.
     */
    
    export interface Emulator extends ObservableI {
    
    João Magalhães's avatar
    João Magalhães committed
        /**
         * Obtains the descriptive name of the emulator.
         *
         * @returns The descriptive name of the emulator.
         */
    
        getName(): string;
    
    João Magalhães's avatar
    João Magalhães committed
    
        /**
         * Obtains a semantic version string for the current
         * version of the emulator.
         *
         * @returns The semantic version string.
         * @see {@link https://semver.org}
         */
    
        getVersion(): string;
    
    João Magalhães's avatar
    João Magalhães committed
    
        /**
         * Obtains a URL to the page describing the current version
         * of the emulator.
         *
         * @returns A URL to the page describing the current version
         * of the emulator.
         */
    
        getVersionUrl(): string;
    
    
        /**
         * Obtains the pixel format of the emulator's display
         * image buffer (eg: RGB).
         *
         * @returns The pixel format used for the emulator's
         * image buffer.
         */
    
        getPixelFormat(): PixelFormat;
    
    
        /**
         * Obtains the complete image buffer as a sequence of
         * bytes that respects the current pixel format from
         * `getPixelFormat()`. This method returns an in memory
         * pointer to the heap and not a copy.
         *
         * @returns The byte based image buffer that respects
         * the emulator's pixel format.
         */
    
        getImageBuffer(): Uint8Array;
    
    
        /**
         * Obtains information about the ROM that is currently
         * loaded in the emulator.
         *
         * @returns Structure containing the information about
         * the ROM that is currently loaded in the emulator.
         */
    
        getRomInfo(): RomInfo;
    
    
        /**
         * Returns the current logic framerate of the running
         * emulator.
         *
         * @return The current logic framerate of the running
         * emulator.
         */
    
        getFramerate(): number;
    
    
        /**
         * Toggle the running state of the emulator between paused
         * and running, prevents consumers from the need to access
         * the current running state of the emulator to implement
         * a logic toggle.
         */
    
        toggleRunning(): void;
        pause(): void;
        resume(): void;
    
    João Magalhães's avatar
    João Magalhães committed
    
        /**
         * Resets the emulator machine to the start state and
         * re-loads the ROM that is currently set in the emulator.
         */
    
    }
    
    /**
     * Enumeration that describes the multiple pixel
     * formats and the associated size in bytes.
     */
    export enum PixelFormat {
        RGB = 3,
        RGBA = 4
    
    type AppProps = {
    
        emulator: Emulator;
    
        backgrounds?: string[];
    };
    
    
    export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
    
        const [paused, setPaused] = useState(false);
    
        const [fullscreen, setFullscreen] = useState(false);
    
        const [backgroundIndex, setBackgroundIndex] = useState(0);
    
        const [romInfo, setRomInfo] = useState<RomInfo>({});
        const [framerate, setFramerate] = useState(0);
    
        const [keyaction, setKeyaction] = useState<string | null>(null);
    
        const frameRef = useRef<boolean>(false);
    
        const errorRef = useRef<boolean>(false);
    
        const getPauseText = () => (paused ? "Resume" : "Pause");
        const getPauseIcon = () =>
            paused ? require("../res/play.svg") : require("../res/pause.svg");
    
        const getBackground = () => backgrounds[backgroundIndex];
    
        const onPauseClick = () => {
            emulator.toggleRunning();
            setPaused(!paused);
    
        const onResetClick = () => {
            emulator.reset();
        };
    
    João Magalhães's avatar
    João Magalhães committed
        const onFullscreenClick = () => {
    
            setFullscreen(!fullscreen);
    
        const onThemeClick = () => {
            setBackgroundIndex((backgroundIndex + 1) % backgrounds.length);
        };
    
        const onMinimize = () => {
            setFullscreen(!fullscreen);
        };
    
    João Magalhães's avatar
    João Magalhães committed
        const onDrawHandler = (handler: DrawHandler) => {
    
            if (frameRef.current) return;
            frameRef.current = true;
    
            emulator.bind("frame", () => {
    
                handler(emulator.getImageBuffer(), PixelFormat.RGB);
    
                setFramerate(emulator.getFramerate());
    
        const onClearHandler = (handler: ClearHandler) => {
            if (errorRef.current) return;
            errorRef.current = true;
            emulator.bind("error", async () => {
                await handler(undefined, require("../res/storm.png"), 0.2);
            });
        };
    
        const onKeyDown = (event: KeyboardEvent) => {};
    
        useEffect(() => {
            document.body.style.backgroundColor = `#${getBackground()}`;
    
        }, [backgroundIndex]);
        useEffect(() => {
            switch (keyaction) {
                case "Escape":
                    setFullscreen(false);
                    setKeyaction(null);
                    break;
                case "Fullscreen":
                    setFullscreen(!fullscreen);
                    setKeyaction(null);
                    break;
            }
        }, [keyaction]);
    
    João Magalhães's avatar
    João Magalhães committed
        useEffect(() => {
            document.addEventListener("keydown", (event) => {
                if (event.key === "Escape") {
    
                    setKeyaction("Escape");
    
                    event.stopPropagation();
                    event.preventDefault();
                }
                if (event.key === "f" && event.ctrlKey === true) {
    
                    setKeyaction("Fullscreen");
    
                    event.stopPropagation();
                    event.preventDefault();
    
            emulator.bind("booted", () => {
    
                const romInfo = emulator.getRomInfo();
                setRomInfo(romInfo);
            });
    
    João Magalhães's avatar
    João Magalhães committed
        }, []);
    
            <div className="app">
    
                <Modal />
    
                <Footer color={getBackground()}>
                    Built with ❤️ by{" "}
    
    João Magalhães's avatar
    João Magalhães committed
                    <Link href="https://joao.me" target="_blank">
    
    João Magalhães's avatar
    João Magalhães committed
                    </Link>
    
                </Footer>
    
                <PanelSplit
                    left={
    
                        <div style={{ marginTop: 78 }}>
    
                            <Display
                                fullscreen={fullscreen}
                                onDrawHandler={onDrawHandler}
    
                                onClearHandler={onClearHandler}
    
                                onMinimize={onMinimize}
                            />
    
                        text={emulator.getName()}
                        version={emulator.getVersion()}
                        versionUrl={emulator.getVersionUrl()}
    
                        iconSrc={require("../res/thunder.png")}
                    ></Title>
    
                    <Section>
                        <Paragraph>
                            This is a{" "}
                            <Link
                                href="https://en.wikipedia.org/wiki/Game_Boy"
                                target="_blank"
                            >
                                Game Boy
                            </Link>{" "}
                            emulator built using the{" "}
                            <Link href="https://www.rust-lang.org" target="_blank">
                                Rust Programming Language
                            </Link>{" "}
                            and is running inside this browser with the help of{" "}
                            <Link href="https://webassembly.org/" target="_blank">
                                WebAssembly
                            </Link>
                            .
                        </Paragraph>
                        <Paragraph>
                            You can check the source code of it at{" "}
                            <Link
                                href="https://gitlab.stage.hive.pt/joamag/boytacean"
                                target="_blank"
                            >
                                GitLab
                            </Link>
                            .
                        </Paragraph>
                        <Paragraph>
                            TIP: Drag and Drop ROM files to the Browser to load the
                            ROM.
                        </Paragraph>
                    </Section>
    
                    <Section>
    
                        <ButtonContainer>
                            <Button
    
                                text={getPauseText()}
                                image={getPauseIcon()}
                                imageAlt="pause"
                                onClick={onPauseClick}
    
                            />
                            <Button
                                text={"Reset"}
                                image={require("../res/reset.svg")}
                                imageAlt="reset"
                                onClick={onResetClick}
                            />
    
    João Magalhães's avatar
    João Magalhães committed
                            <Button
                                text={"Fullscreen"}
                                image={require("../res/maximise.svg")}
                                imageAlt="maximise"
                                onClick={onFullscreenClick}
                            />
    
                            <Button
                                text={"Theme"}
                                image={require("../res/marker.svg")}
                                imageAlt="marker"
                                onClick={onThemeClick}
                            />
                        </ButtonContainer>
    
                            <Pair
                                key="rom"
                                name={"ROM"}
                                value={romInfo.name ?? "-"}
                            />
                            <Pair
                                key="rom-size"
                                name={"ROM Size"}
                                value={romInfo.name ? `${romInfo.size} bytes` : "-"}
                            />
                            <Pair
                                key="rom-type"
                                name={"ROM Type"}
                                value={
                                    romInfo.extra?.romType
                                        ? `${romInfo.extra?.romType}`
                                        : "-"
                                }
                            />
                            <Pair
                                key="framerate"
                                name={"Framerate"}
                                value={`${framerate} fps`}
                            />
    
                            <Pair
                                key="button-tobias"
                                name={"Button Increment"}
                                valueNode={
                                    <ButtonIncrement
                                        value={200}
                                        delta={100}
                                        min={0}
                                        suffix={"Hz"}
                                    />
                                }
                            />
                            <Pair
                                key="button-cpu"
                                name={"Button Switch"}
                                valueNode={
                                    <ButtonSwitch
                                        options={["NEO", "CLASSIC"]}
                                        size={"large"}
                                        style={["simple"]}
                                        onChange={(v) => alert(v)}
                                    />
                                }
                            />
                        </Info>
                    </Section>
                </PanelSplit>
    
    export const startApp = (
        element: string,
        emulator: Emulator,
        backgrounds: string[]
    ) => {
    
        const elementRef = document.getElementById(element);
        if (!elementRef) {
            return;
        }
    
        const root = ReactDOM.createRoot(elementRef);
    
        root.render(<App emulator={emulator} backgrounds={backgrounds} />);
    
    
    export default App;