From 9686eda3a89da7858a127631ddc1f4ccc16851ee Mon Sep 17 00:00:00 2001 From: Will Scullin Date: Thu, 9 Jun 2022 12:22:59 -0700 Subject: [PATCH] Preact printer (#129) --- js/components/ControlButton.tsx | 8 +- js/components/ControlStrip.tsx | 5 + js/components/Printer.tsx | 107 +++++++++++++++++++++ js/components/css/App.module.css | 12 ++- js/components/css/ControlButton.module.css | 3 + js/components/css/Modal.module.css | 7 ++ js/components/css/Printer.module.css | 9 ++ 7 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 js/components/Printer.tsx create mode 100644 js/components/css/ControlButton.module.css create mode 100644 js/components/css/Printer.module.css diff --git a/js/components/ControlButton.tsx b/js/components/ControlButton.tsx index 35e4ce6..b3ddfd7 100644 --- a/js/components/ControlButton.tsx +++ b/js/components/ControlButton.tsx @@ -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; } @@ -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) => ( ); diff --git a/js/components/ControlStrip.tsx b/js/components/ControlStrip.tsx index e4bec17..eeea766 100644 --- a/js/components/ControlStrip.tsx +++ b/js/components/ControlStrip.tsx @@ -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(); 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) => { +
diff --git a/js/components/Printer.tsx b/js/components/Printer.tsx new file mode 100644 index 0000000..e0594d6 --- /dev/null +++ b/js/components/Printer.tsx @@ -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(() => ({ + 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 ( + <> + + +
+                        {content}
+                    
+
+ + + Download Raw + + + + +
+ 0} + /> + + ); +}; diff --git a/js/components/css/App.module.css b/js/components/css/App.module.css index 4df33f7..8e96d27 100644 --- a/js/components/css/App.module.css +++ b/js/components/css/App.module.css @@ -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; +} diff --git a/js/components/css/ControlButton.module.css b/js/components/css/ControlButton.module.css new file mode 100644 index 0000000..b333f9b --- /dev/null +++ b/js/components/css/ControlButton.module.css @@ -0,0 +1,3 @@ +.active { + color: #0f0; +} diff --git a/js/components/css/Modal.module.css b/js/components/css/Modal.module.css index 32107a4..f287e68 100644 --- a/js/components/css/Modal.module.css +++ b/js/components/css/Modal.module.css @@ -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; } diff --git a/js/components/css/Printer.module.css b/js/components/css/Printer.module.css new file mode 100644 index 0000000..1b0463e --- /dev/null +++ b/js/components/css/Printer.module.css @@ -0,0 +1,9 @@ +.printer { + background: white; + font: fixed; + width: 60em; + padding: 5em; + height: 60vh; + overflow: auto; + border: 2px inset #f0edd0; +}