Preact printer (#129)

This commit is contained in:
Will Scullin 2022-06-09 12:22:59 -07:00 committed by GitHub
parent c9b7987c4c
commit 9686eda3a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 145 additions and 6 deletions

View File

@ -1,4 +1,7 @@
import { h, JSX } from 'preact';
import cs from 'classnames';
import styles from './css/ControlButton.module.css';
/**
* Interface for ControlButton.
@ -7,6 +10,7 @@ export interface ControlButtonProps {
icon: string;
title: string;
disabled?: boolean;
active?: boolean;
onClick: JSX.MouseEventHandler<HTMLButtonElement>;
}
@ -18,8 +22,8 @@ export interface ControlButtonProps {
* @param onClick Click callback
* @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} >
<i className={`fas fa-${icon}`}></i>
<i className={cs('fas', `fa-${icon}`, { [styles.active]: active })}></i>
</button>
);

View File

@ -7,6 +7,7 @@ import { AudioControl } from './AudioControl';
import { OptionsModal} from './OptionsModal';
import { OptionsContext } from './OptionsContext';
import { PauseControl } from './PauseControl';
import { Printer } from './Printer';
import { ControlButton } from './ControlButton';
import { Apple2 as Apple2Impl } from '../apple2';
import { JoyStick } from '../ui/joystick';
@ -14,6 +15,7 @@ import { Screen, SCREEN_FULL_PAGE } from '../ui/screen';
import { System } from '../ui/system';
import styles from './css/ControlStrip.module.css';
import Apple2IO from 'js/apple2io';
const README = 'https://github.com/whscullin/apple2js#readme';
@ -33,12 +35,14 @@ interface ControlStripProps {
*/
export const ControlStrip = ({ apple2, e }: ControlStripProps) => {
const [showOptions, setShowOptions] = useState(false);
const [io, setIO] = useState<Apple2IO>();
const options = useContext(OptionsContext);
useEffect(() => {
if (apple2) {
const io = apple2.getIO();
const vm = apple2.getVideoModes();
setIO(io);
const system = new System(io, e);
options.addOptions(system);
@ -85,6 +89,7 @@ export const ControlStrip = ({ apple2, e }: ControlStripProps) => {
<CPUMeter apple2={apple2} />
<PauseControl apple2={apple2} />
<AudioControl apple2={apple2} />
<Printer io={io} slot={1} />
<div style={{flexGrow: 1}} />
<ControlButton onClick={doReadme} title="About" icon="info" />
<ControlButton onClick={doShowOptions} title="Options (F4)" icon="cog" />

107
js/components/Printer.tsx Normal file
View 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}
/>
</>
);
};

View File

@ -9,7 +9,7 @@ body {
background: black;
}
button {
button, a[role="button"] {
background: #44372C;
color: #fff;
padding: 2px 8px;
@ -19,16 +19,20 @@ button {
min-width: 75px;
}
button:hover {
button:hover, a[role="button"]:hover {
background-color: #55473D;
border: 1px outset #66594E;
}
button:active {
button:active, a[role="button"]:active {
background-color: #22150A;
border: 1px outset #44372C;
}
button:focus {
button:focus, a[role="button"]:focus {
outline: none;
}
a[role="button"] {
text-decoration: none;
}

View File

@ -0,0 +1,3 @@
.active {
color: #0f0;
}

View File

@ -35,6 +35,7 @@
padding: 5px 11px;
border: 1px outset #66594E;
border-radius: 3px;
user-select: none;
}
.modalHeader button {
@ -60,4 +61,10 @@
.modalFooter {
text-align: right;
user-select: none;
}
.modalFooter a[role="button"], .modalFooter button {
margin: 0 0 0 5px;
min-width: 75px;
}

View File

@ -0,0 +1,9 @@
.printer {
background: white;
font: fixed;
width: 60em;
padding: 5em;
height: 60vh;
overflow: auto;
border: 2px inset #f0edd0;
}