2022-05-10 13:52:06 +00:00
|
|
|
import { h } from 'preact';
|
|
|
|
import cs from 'classnames';
|
2022-06-20 02:42:34 +00:00
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
2022-05-10 13:52:06 +00:00
|
|
|
import { Apple2 as Apple2Impl } from '../apple2';
|
|
|
|
import { ControlStrip } from './ControlStrip';
|
2022-07-14 03:34:50 +00:00
|
|
|
import { Debugger } from './debugger/Debugger';
|
2022-06-05 17:57:04 +00:00
|
|
|
import { ErrorModal } from './ErrorModal';
|
2022-05-10 13:52:06 +00:00
|
|
|
import { Inset } from './Inset';
|
|
|
|
import { Keyboard } from './Keyboard';
|
2022-07-10 14:58:29 +00:00
|
|
|
import { LanguageCard } from './LanguageCard';
|
2022-05-12 00:21:21 +00:00
|
|
|
import { Mouse } from './Mouse';
|
2022-05-10 13:52:06 +00:00
|
|
|
import { Screen } from './Screen';
|
|
|
|
import { Drives } from './Drives';
|
2022-05-12 14:59:12 +00:00
|
|
|
import { Slinky } from './Slinky';
|
|
|
|
import { ThunderClock } from './ThunderClock';
|
2022-06-16 01:44:58 +00:00
|
|
|
import { Videoterm } from './Videoterm';
|
2022-06-12 16:05:01 +00:00
|
|
|
import { spawn, Ready } from './util/promises';
|
2022-05-10 13:52:06 +00:00
|
|
|
|
2022-06-03 22:30:39 +00:00
|
|
|
import styles from './css/Apple2.module.css';
|
|
|
|
|
2022-07-06 21:00:18 +00:00
|
|
|
declare global {
|
|
|
|
interface Window {
|
|
|
|
apple2: Apple2Impl;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-10 13:52:06 +00:00
|
|
|
/**
|
|
|
|
* 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) => {
|
2022-06-05 17:57:04 +00:00
|
|
|
const { e, enhanced, sectors } = props;
|
2022-07-17 03:50:15 +00:00
|
|
|
const screenRef = useRef<HTMLCanvasElement>(null);
|
2022-05-10 13:52:06 +00:00
|
|
|
const [apple2, setApple2] = useState<Apple2Impl>();
|
2022-06-01 13:28:05 +00:00
|
|
|
const [error, setError] = useState<unknown>();
|
2022-06-12 16:42:01 +00:00
|
|
|
const [ready, setReady] = useState(false);
|
2022-07-23 19:32:40 +00:00
|
|
|
const [showDebug, setShowDebug] = useState(false);
|
2022-06-05 17:57:04 +00:00
|
|
|
const drivesReady = useMemo(() => new Ready(setError), []);
|
2022-05-10 13:52:06 +00:00
|
|
|
|
2022-07-06 21:00:18 +00:00
|
|
|
const io = apple2?.getIO();
|
|
|
|
const cpu = apple2?.getCPU();
|
|
|
|
const vm = apple2?.getVideoModes();
|
2022-07-10 14:58:29 +00:00
|
|
|
const rom = apple2?.getROM();
|
2022-07-06 21:00:18 +00:00
|
|
|
|
|
|
|
const doPaste = useCallback((event: Event) => {
|
2022-07-17 03:50:15 +00:00
|
|
|
if (
|
|
|
|
(document.activeElement !== screenRef.current) &&
|
|
|
|
(document.activeElement !== document.body)
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
2022-07-06 21:00:18 +00:00
|
|
|
if (io) {
|
|
|
|
const paste = (event.clipboardData || window.clipboardData)?.getData('text');
|
|
|
|
if (paste) {
|
|
|
|
io.setKeyBuffer(paste);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
}, [io]);
|
|
|
|
|
|
|
|
const doCopy = useCallback((event: Event) => {
|
2022-07-17 03:50:15 +00:00
|
|
|
if (
|
|
|
|
(document.activeElement !== screenRef.current) &&
|
|
|
|
(document.activeElement !== document.body)
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
2022-07-06 21:00:18 +00:00
|
|
|
if (vm) {
|
|
|
|
event.clipboardData?.setData('text/plain', vm.getText());
|
|
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
}, [vm]);
|
|
|
|
|
2022-05-10 13:52:06 +00:00
|
|
|
useEffect(() => {
|
2022-07-17 03:50:15 +00:00
|
|
|
if (screenRef.current) {
|
2022-05-10 13:52:06 +00:00
|
|
|
const options = {
|
2022-07-17 03:50:15 +00:00
|
|
|
canvas: screenRef.current,
|
2022-05-31 15:38:40 +00:00
|
|
|
tick: () => { /* do nothing */ },
|
2022-05-10 13:52:06 +00:00
|
|
|
...props,
|
|
|
|
};
|
|
|
|
const apple2 = new Apple2Impl(options);
|
Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
const controller = spawn(async (signal) => {
|
2022-06-05 17:57:04 +00:00
|
|
|
try {
|
|
|
|
await apple2.ready;
|
Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
if (signal.aborted) {
|
|
|
|
return;
|
|
|
|
}
|
2022-06-05 17:57:04 +00:00
|
|
|
setApple2(apple2);
|
|
|
|
await drivesReady.ready;
|
Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
if (signal.aborted) {
|
2022-06-12 16:42:01 +00:00
|
|
|
setApple2(undefined);
|
Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-06-05 17:57:04 +00:00
|
|
|
apple2.reset();
|
|
|
|
apple2.run();
|
|
|
|
} catch (e) {
|
|
|
|
setError(e);
|
|
|
|
}
|
2022-06-12 16:42:01 +00:00
|
|
|
setReady(true);
|
2022-06-12 16:05:01 +00:00
|
|
|
});
|
2022-07-06 21:00:18 +00:00
|
|
|
|
|
|
|
window.apple2 = apple2;
|
|
|
|
|
2022-06-12 16:42:01 +00:00
|
|
|
return () => controller.abort();
|
2022-05-10 13:52:06 +00:00
|
|
|
}
|
2022-06-05 17:57:04 +00:00
|
|
|
}, [props, drivesReady]);
|
2022-05-10 13:52:06 +00:00
|
|
|
|
2022-07-06 21:00:18 +00:00
|
|
|
useEffect(() => {
|
2022-07-17 03:50:15 +00:00
|
|
|
const { current } = screenRef;
|
|
|
|
|
2022-07-06 21:00:18 +00:00
|
|
|
window.addEventListener('paste', doPaste);
|
|
|
|
window.addEventListener('copy', doCopy);
|
|
|
|
|
2022-07-17 03:50:15 +00:00
|
|
|
current?.addEventListener('paste', doPaste);
|
|
|
|
current?.addEventListener('copy', doCopy);
|
|
|
|
|
2022-07-06 21:00:18 +00:00
|
|
|
return () => {
|
|
|
|
window.removeEventListener('paste', doPaste);
|
|
|
|
window.removeEventListener('copy', doCopy);
|
2022-07-17 03:50:15 +00:00
|
|
|
|
|
|
|
current?.removeEventListener('paste', doPaste);
|
|
|
|
current?.removeEventListener('copy', doCopy);
|
2022-07-06 21:00:18 +00:00
|
|
|
};
|
|
|
|
}, [doCopy, doPaste]);
|
|
|
|
|
2022-06-20 02:42:34 +00:00
|
|
|
const toggleDebugger = useCallback(() => {
|
|
|
|
setShowDebug((on) => !on);
|
|
|
|
}, []);
|
|
|
|
|
2022-05-10 13:52:06 +00:00
|
|
|
return (
|
2022-06-20 02:42:34 +00:00
|
|
|
<div className={styles.container}>
|
|
|
|
<div
|
|
|
|
className={cs(styles.outer, { apple2e: e, [styles.ready]: ready })}
|
|
|
|
>
|
2022-07-17 03:50:15 +00:00
|
|
|
<Screen screenRef={screenRef} />
|
2022-07-10 14:58:29 +00:00
|
|
|
{!e ? <LanguageCard cpu={cpu} io={io} rom={rom} slot={0} /> : null}
|
2022-06-20 02:42:34 +00:00
|
|
|
<Slinky io={io} slot={2} />
|
|
|
|
{!e ? <Videoterm io={io} slot={3} /> : null}
|
2022-07-17 03:50:15 +00:00
|
|
|
<Mouse cpu={cpu} screenRef={screenRef} io={io} slot={4} />
|
2022-06-20 02:42:34 +00:00
|
|
|
<ThunderClock io={io} slot={5} />
|
|
|
|
<Inset>
|
|
|
|
<Drives cpu={cpu} io={io} sectors={sectors} enhanced={enhanced} ready={drivesReady} />
|
|
|
|
</Inset>
|
|
|
|
<ControlStrip apple2={apple2} e={e} toggleDebugger={toggleDebugger} />
|
|
|
|
<Inset>
|
|
|
|
<Keyboard apple2={apple2} e={e} />
|
|
|
|
</Inset>
|
|
|
|
<ErrorModal error={error} setError={setError} />
|
|
|
|
</div>
|
2022-07-14 03:34:50 +00:00
|
|
|
{showDebug ? <Debugger apple2={apple2} /> : null}
|
2022-05-10 13:52:06 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|