diff --git a/.eslintrc.json b/.eslintrc.json
index cbbbcc8..442f3ea 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -20,7 +20,8 @@
"prefer-const": [
"error"
],
- "semi": [
+ "semi": "off",
+ "@typescript-eslint/semi": [
"error",
"always"
],
@@ -52,7 +53,8 @@
"error"
]
}
- ]
+ ],
+ "@typescript-eslint/require-await": ["error"]
},
"env": {
"builtin": true,
@@ -120,5 +122,10 @@
}
}
],
- "ignorePatterns": ["coverage/**/*"]
+ "ignorePatterns": ["coverage/**/*"],
+ "settings": {
+ "react": {
+ "pragma": "h"
+ }
+ }
}
diff --git a/babel.config.js b/babel.config.js
index 442aa3c..69cbd06 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -9,5 +9,19 @@ module.exports = {
},
},
],
+ [
+ '@babel/typescript',
+ {
+ jsxPragma: 'h'
+ }
+ ],
],
+ plugins: [
+ [
+ '@babel/plugin-transform-react-jsx', {
+ pragma: 'h',
+ pragmaFrag: 'Fragment',
+ }
+ ]
+ ]
};
diff --git a/css/apple2.css b/css/apple2.css
index 3952ebc..916ef7f 100644
--- a/css/apple2.css
+++ b/css/apple2.css
@@ -122,6 +122,10 @@ body {
height: 16px;
}
+.disk-light.on {
+ background-image: url(red-on-16.png);
+}
+
.disk-label {
color: #000;
font-family: sans-serif;
@@ -583,6 +587,7 @@ button:focus {
#reset-row .inset {
margin: 0;
+ width: 604px;
}
#reset {
@@ -650,7 +655,6 @@ button:focus {
#options-modal {
width: 300px;
- line-height: 1.75em;
}
#options-modal h3 {
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..37fa90a
--- /dev/null
+++ b/index.html
@@ -0,0 +1,10 @@
+
+
+ PreApple II
+
+
+
+
+
+
+
diff --git a/js/apple2.ts b/js/apple2.ts
index d72aced..1527895 100644
--- a/js/apple2.ts
+++ b/js/apple2.ts
@@ -33,25 +33,26 @@ import { processGamepad } from './ui/gamepad';
export interface Apple2Options {
characterRom: string;
- enhanced: boolean,
- e: boolean,
- gl: boolean,
- rom: string,
- canvas: HTMLCanvasElement,
- tick: () => void,
+ enhanced: boolean;
+ e: boolean;
+ gl: boolean;
+ rom: string;
+ canvas: HTMLCanvasElement;
+ tick: () => void;
}
export interface Stats {
- frames: number,
- renderedFrames: number,
+ cycles: number;
+ frames: number;
+ renderedFrames: number;
}
interface State {
- cpu: CpuState,
- vm: VideoModesState,
- io: Apple2IOState,
- mmu?: MMUState,
- ram?: RAMState[],
+ cpu: CpuState;
+ vm: VideoModesState;
+ io: Apple2IOState;
+ mmu?: MMUState;
+ ram?: RAMState[];
}
export class Apple2 implements Restorable, DebuggerContainer {
@@ -78,6 +79,7 @@ export class Apple2 implements Restorable, DebuggerContainer {
private tick: () => void;
private stats: Stats = {
+ cycles: 0,
frames: 0,
renderedFrames: 0
};
@@ -186,6 +188,7 @@ export class Apple2 implements Restorable, DebuggerContainer {
this.stats.renderedFrames++;
}
}
+ this.stats.cycles = this.cpu.getCycles();
this.stats.frames++;
this.io.tick();
this.tick();
diff --git a/js/components/App.tsx b/js/components/App.tsx
new file mode 100644
index 0000000..0bac016
--- /dev/null
+++ b/js/components/App.tsx
@@ -0,0 +1,35 @@
+import 'preact/debug';
+import { h, Fragment } from 'preact';
+import { Header } from './Header';
+import { Apple2 } from './Apple2';
+import { usePrefs } from './hooks/usePrefs';
+import { SYSTEM_TYPE_APPLE2E } from '../ui/system';
+import { SCREEN_GL } from '../ui/screen';
+import { defaultSystem, systemTypes } from './util/systems';
+
+/**
+ * Top level application component, provides the parameters
+ * needed by the Apple2 component to bootstrap itself.
+ *
+ * @returns Application component
+ */
+export const App = () => {
+ const prefs = usePrefs();
+ const systemType = prefs.readPref(SYSTEM_TYPE_APPLE2E, 'apple2enh');
+ const gl = prefs.readPref(SCREEN_GL, 'true') === 'true';
+
+ const system = {
+ ...defaultSystem,
+ ...(systemTypes[systemType] || {})
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/js/components/Apple2.tsx b/js/components/Apple2.tsx
new file mode 100644
index 0000000..317355d
--- /dev/null
+++ b/js/components/Apple2.tsx
@@ -0,0 +1,69 @@
+import { h } from 'preact';
+import cs from 'classnames';
+import {useEffect, useRef, useState } from 'preact/hooks';
+import { Apple2 as Apple2Impl } from '../apple2';
+import Apple2IO from '../apple2io';
+import { ControlStrip } from './ControlStrip';
+import { Inset } from './Inset';
+import { Keyboard } from './Keyboard';
+import { Screen } from './Screen';
+import { Drives } from './Drives';
+
+/**
+ * Interface for the Apple2 component.
+ */
+export interface Apple2Props {
+ characterRom: string;
+ enhanced: boolean;
+ e: boolean;
+ gl: boolean;
+ rom: string;
+ sectors: number;
+}
+
+/**
+ * Component to bind various UI components together to form
+ * the application layout. Includes the screen, drives,
+ * emulator controls and keyboard. Bootstraps the core
+ * Apple2 emulator.
+ *
+ * @param props Apple2 initialization props
+ * @returns
+ */
+export const Apple2 = (props: Apple2Props) => {
+ const { e, sectors } = props;
+ const screen = useRef(null);
+ const [apple2, setApple2] = useState();
+ const [io, setIO] = useState();
+
+ useEffect(() => {
+ if (screen.current) {
+ const options = {
+ canvas: screen.current,
+ tick: () => {},
+ ...props,
+ };
+ const apple2 = new Apple2Impl(options);
+ setApple2(apple2);
+ apple2.ready.then(() => {
+ const io = apple2.getIO();
+ setIO(io);
+ apple2.getCPU().reset();
+ apple2.run();
+ }).catch(error => console.error(error));
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/js/components/CPUMeter.tsx b/js/components/CPUMeter.tsx
new file mode 100644
index 0000000..43c206b
--- /dev/null
+++ b/js/components/CPUMeter.tsx
@@ -0,0 +1,72 @@
+import { h } from 'preact';
+import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
+import { Apple2 as Apple2Impl } from '../apple2';
+import type { Stats } from '../apple2';
+
+/**
+ * Interface for CPUMeter.
+ */
+export interface CPUMeterProps {
+ apple2: Apple2Impl | undefined;
+}
+
+/**
+ * A simple display that can cycle between emulator Khz
+ * performance, frames/second and rendered frames/second.
+ *
+ * @param apple2 Apple2 object
+ * @returns CPU Meter component
+ */
+export const CPUMeter = ({ apple2 }: CPUMeterProps) => {
+ const lastStats = useRef({
+ frames: 0,
+ renderedFrames: 0,
+ cycles: 0,
+ });
+ const lastTime = useRef(Date.now());
+ const [khz, setKhz] = useState(0);
+ const [fps, setFps] = useState(0);
+ const [rps, setRps] = useState(0);
+ const [mode, setMode] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ const { cycles, frames, renderedFrames } = lastStats.current;
+ const stats = apple2?.getStats();
+ const time = Date.now();
+ const delta = time - lastTime.current;
+ if (stats) {
+ setKhz(
+ Math.floor(
+ (stats.cycles - cycles) / delta
+ )
+ );
+ setFps(
+ Math.floor(
+ (stats.frames - frames) / delta * 1000
+ )
+ );
+ setRps(
+ Math.floor(
+ (stats.renderedFrames - renderedFrames) / delta * 1000
+ )
+ );
+ lastStats.current = { ...stats };
+ lastTime.current = time;
+ }
+ }, 1000);
+ return () => clearInterval(interval);
+ }, [apple2]);
+
+ const onClick = useCallback(() => {
+ setMode((mode) => (mode + 1) % 3);
+ }, []);
+
+ return (
+
+ {mode === 0 && `${khz} Khz`}
+ {mode === 1 && `${fps} fps`}
+ {mode === 2 && `${rps} rps`}
+
+ );
+};
diff --git a/js/components/ControlButton.tsx b/js/components/ControlButton.tsx
new file mode 100644
index 0000000..c39f9cd
--- /dev/null
+++ b/js/components/ControlButton.tsx
@@ -0,0 +1,24 @@
+import { h, JSX } from 'preact';
+
+/**
+ * Interface for ControlButton.
+ */
+export interface ControlButtonProps {
+ icon: string;
+ title: string;
+ onClick: JSX.MouseEventHandler;
+}
+
+/**
+ * Simple button with an icon, tooltip text and a callback.
+ *
+ * @param icon FontAwesome icon name
+ * @param title Tooltip text
+ * @param onClick Click callback
+ * @returns Control Button component
+ */
+export const ControlButton = ({ icon, title, onClick }: ControlButtonProps) => (
+
+);
diff --git a/js/components/ControlStrip.tsx b/js/components/ControlStrip.tsx
new file mode 100644
index 0000000..ddf4de9
--- /dev/null
+++ b/js/components/ControlStrip.tsx
@@ -0,0 +1,138 @@
+import { h } from 'preact';
+import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
+import { CPUMeter } from './CPUMeter';
+import { Inset } from './Inset';
+import { useHotKey } from './hooks/useHotKey';
+import { Apple2 as Apple2Impl } from '../apple2';
+import { Audio, SOUND_ENABLED_OPTION } from '../ui/audio';
+import { OptionsModal} from './OptionsModal';
+import { OptionsContext } from './OptionsContext';
+import { ControlButton } from './ControlButton';
+import { JoyStick } from '../ui/joystick';
+import { Screen, SCREEN_FULL_PAGE } from '../ui/screen';
+import { System } from '../ui/system';
+
+const README = 'https://github.com/whscullin/apple2js#readme';
+
+interface ControlStripProps {
+ apple2: Apple2Impl | undefined;
+ e: boolean;
+}
+
+/**
+ * Strip containing containing controls for various system
+ * characteristics, like CPU speed, audio, and the system
+ * options panel.
+ *
+ * @param apple2 Apple2 object
+ * @param e Whether or not this is a //e
+ * @returns ControlStrip component
+ */
+export const ControlStrip = ({ apple2, e }: ControlStripProps) => {
+ const [running, setRunning] = useState(true);
+ const [audio, setAudio] = useState