mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Preact printer (#129)
This commit is contained in:
parent
c9b7987c4c
commit
9686eda3a8
@ -1,4 +1,7 @@
|
|||||||
import { h, JSX } from 'preact';
|
import { h, JSX } from 'preact';
|
||||||
|
import cs from 'classnames';
|
||||||
|
|
||||||
|
import styles from './css/ControlButton.module.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for ControlButton.
|
* Interface for ControlButton.
|
||||||
@ -7,6 +10,7 @@ export interface ControlButtonProps {
|
|||||||
icon: string;
|
icon: string;
|
||||||
title: string;
|
title: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
active?: boolean;
|
||||||
onClick: JSX.MouseEventHandler<HTMLButtonElement>;
|
onClick: JSX.MouseEventHandler<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,8 +22,8 @@ export interface ControlButtonProps {
|
|||||||
* @param onClick Click callback
|
* @param onClick Click callback
|
||||||
* @returns Control Button component
|
* @returns Control Button component
|
||||||
*/
|
*/
|
||||||
export const ControlButton = ({ icon, title, onClick, ...props }: ControlButtonProps) => (
|
export const ControlButton = ({ active, icon, title, onClick, ...props }: ControlButtonProps) => (
|
||||||
<button onClick={onClick} title={title} {...props} >
|
<button onClick={onClick} title={title} {...props} >
|
||||||
<i className={`fas fa-${icon}`}></i>
|
<i className={cs('fas', `fa-${icon}`, { [styles.active]: active })}></i>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -7,6 +7,7 @@ import { AudioControl } from './AudioControl';
|
|||||||
import { OptionsModal} from './OptionsModal';
|
import { OptionsModal} from './OptionsModal';
|
||||||
import { OptionsContext } from './OptionsContext';
|
import { OptionsContext } from './OptionsContext';
|
||||||
import { PauseControl } from './PauseControl';
|
import { PauseControl } from './PauseControl';
|
||||||
|
import { Printer } from './Printer';
|
||||||
import { ControlButton } from './ControlButton';
|
import { ControlButton } from './ControlButton';
|
||||||
import { Apple2 as Apple2Impl } from '../apple2';
|
import { Apple2 as Apple2Impl } from '../apple2';
|
||||||
import { JoyStick } from '../ui/joystick';
|
import { JoyStick } from '../ui/joystick';
|
||||||
@ -14,6 +15,7 @@ import { Screen, SCREEN_FULL_PAGE } from '../ui/screen';
|
|||||||
import { System } from '../ui/system';
|
import { System } from '../ui/system';
|
||||||
|
|
||||||
import styles from './css/ControlStrip.module.css';
|
import styles from './css/ControlStrip.module.css';
|
||||||
|
import Apple2IO from 'js/apple2io';
|
||||||
|
|
||||||
const README = 'https://github.com/whscullin/apple2js#readme';
|
const README = 'https://github.com/whscullin/apple2js#readme';
|
||||||
|
|
||||||
@ -33,12 +35,14 @@ interface ControlStripProps {
|
|||||||
*/
|
*/
|
||||||
export const ControlStrip = ({ apple2, e }: ControlStripProps) => {
|
export const ControlStrip = ({ apple2, e }: ControlStripProps) => {
|
||||||
const [showOptions, setShowOptions] = useState(false);
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
|
const [io, setIO] = useState<Apple2IO>();
|
||||||
const options = useContext(OptionsContext);
|
const options = useContext(OptionsContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (apple2) {
|
if (apple2) {
|
||||||
const io = apple2.getIO();
|
const io = apple2.getIO();
|
||||||
const vm = apple2.getVideoModes();
|
const vm = apple2.getVideoModes();
|
||||||
|
setIO(io);
|
||||||
|
|
||||||
const system = new System(io, e);
|
const system = new System(io, e);
|
||||||
options.addOptions(system);
|
options.addOptions(system);
|
||||||
@ -85,6 +89,7 @@ export const ControlStrip = ({ apple2, e }: ControlStripProps) => {
|
|||||||
<CPUMeter apple2={apple2} />
|
<CPUMeter apple2={apple2} />
|
||||||
<PauseControl apple2={apple2} />
|
<PauseControl apple2={apple2} />
|
||||||
<AudioControl apple2={apple2} />
|
<AudioControl apple2={apple2} />
|
||||||
|
<Printer io={io} slot={1} />
|
||||||
<div style={{flexGrow: 1}} />
|
<div style={{flexGrow: 1}} />
|
||||||
<ControlButton onClick={doReadme} title="About" icon="info" />
|
<ControlButton onClick={doReadme} title="About" icon="info" />
|
||||||
<ControlButton onClick={doShowOptions} title="Options (F4)" icon="cog" />
|
<ControlButton onClick={doShowOptions} title="Options (F4)" icon="cog" />
|
||||||
|
107
js/components/Printer.tsx
Normal file
107
js/components/Printer.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { h, Fragment } from 'preact';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
|
import Apple2IO, { slot } from 'js/apple2io';
|
||||||
|
import Parallel, { ParallelOptions } from 'js/cards/parallel';
|
||||||
|
import { Modal, ModalContent, ModalFooter } from './Modal';
|
||||||
|
|
||||||
|
import styles from './css/Printer.module.css';
|
||||||
|
import { ControlButton } from './ControlButton';
|
||||||
|
import { byte } from 'js/types';
|
||||||
|
|
||||||
|
export interface PrinterProps {
|
||||||
|
io: Apple2IO | undefined;
|
||||||
|
slot: slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Printer = ({ io, slot }: PrinterProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
const raw = useRef(new Uint8Array(1024));
|
||||||
|
const rawLength = useRef(0);
|
||||||
|
const [href, setHref] = useState('');
|
||||||
|
|
||||||
|
const cbs = useMemo<ParallelOptions>(() => ({
|
||||||
|
putChar: (val: byte) => {
|
||||||
|
const ascii = val & 0x7f;
|
||||||
|
const visible = val >= 0x20;
|
||||||
|
const char = String.fromCharCode(ascii);
|
||||||
|
|
||||||
|
if (char === '\r') {
|
||||||
|
// Skip for once
|
||||||
|
} else if (char === '\t') {
|
||||||
|
// possibly not right due to tab stops
|
||||||
|
setContent((content) => content += ' ');
|
||||||
|
} else if (ascii === 0x04) {
|
||||||
|
setContent((content) => content = content.slice(0, -1));
|
||||||
|
return;
|
||||||
|
} else if (visible) {
|
||||||
|
setContent((content) => content += char);
|
||||||
|
}
|
||||||
|
|
||||||
|
raw.current[rawLength.current++] = val;
|
||||||
|
if (rawLength.current > raw.current.length) {
|
||||||
|
const newRaw = new Uint8Array(raw.current.length * 2);
|
||||||
|
newRaw.set(raw.current);
|
||||||
|
raw.current = newRaw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), [rawLength]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (io) {
|
||||||
|
const parallel = new Parallel(cbs);
|
||||||
|
io.setSlot(slot, parallel);
|
||||||
|
}
|
||||||
|
}, [cbs, io, slot]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
const blob = new Blob(
|
||||||
|
[raw.current.slice(0, rawLength.current)],
|
||||||
|
{ type: 'application/octet-stream' }
|
||||||
|
);
|
||||||
|
const href = window.URL.createObjectURL(blob);
|
||||||
|
setHref(href);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const onClear = useCallback(() => {
|
||||||
|
setContent('');
|
||||||
|
rawLength.current = 0;
|
||||||
|
}, []);
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, []);
|
||||||
|
const onOpen = useCallback(() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} title="Printer">
|
||||||
|
<ModalContent>
|
||||||
|
<pre className={styles.printer}>
|
||||||
|
{content}
|
||||||
|
</pre>
|
||||||
|
</ModalContent>
|
||||||
|
<ModalFooter>
|
||||||
|
<a
|
||||||
|
download="raw_printer_output.bin"
|
||||||
|
href={href}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
Download Raw
|
||||||
|
</a>
|
||||||
|
<button onClick={onClear}>Clear</button>
|
||||||
|
<button onClick={onClose}>OK</button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
<ControlButton
|
||||||
|
icon="print"
|
||||||
|
title="Printer"
|
||||||
|
onClick={onOpen}
|
||||||
|
active={content.length > 0}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -9,7 +9,7 @@ body {
|
|||||||
background: black;
|
background: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button, a[role="button"] {
|
||||||
background: #44372C;
|
background: #44372C;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@ -19,16 +19,20 @@ button {
|
|||||||
min-width: 75px;
|
min-width: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover, a[role="button"]:hover {
|
||||||
background-color: #55473D;
|
background-color: #55473D;
|
||||||
border: 1px outset #66594E;
|
border: 1px outset #66594E;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active {
|
button:active, a[role="button"]:active {
|
||||||
background-color: #22150A;
|
background-color: #22150A;
|
||||||
border: 1px outset #44372C;
|
border: 1px outset #44372C;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus {
|
button:focus, a[role="button"]:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a[role="button"] {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
3
js/components/css/ControlButton.module.css
Normal file
3
js/components/css/ControlButton.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.active {
|
||||||
|
color: #0f0;
|
||||||
|
}
|
@ -35,6 +35,7 @@
|
|||||||
padding: 5px 11px;
|
padding: 5px 11px;
|
||||||
border: 1px outset #66594E;
|
border: 1px outset #66594E;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modalHeader button {
|
.modalHeader button {
|
||||||
@ -60,4 +61,10 @@
|
|||||||
|
|
||||||
.modalFooter {
|
.modalFooter {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter a[role="button"], .modalFooter button {
|
||||||
|
margin: 0 0 0 5px;
|
||||||
|
min-width: 75px;
|
||||||
}
|
}
|
||||||
|
9
js/components/css/Printer.module.css
Normal file
9
js/components/css/Printer.module.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.printer {
|
||||||
|
background: white;
|
||||||
|
font: fixed;
|
||||||
|
width: 60em;
|
||||||
|
padding: 5em;
|
||||||
|
height: 60vh;
|
||||||
|
overflow: auto;
|
||||||
|
border: 2px inset #f0edd0;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user