2021-12-21 22:20:45 +00:00
|
|
|
import { render, h, Component, Computer, PeripheralKind } from "copycat/embed";
|
2020-11-12 19:01:50 +00:00
|
|
|
import type { ComponentChild } from "preact";
|
|
|
|
|
|
|
|
import settingsFile from "./mount/.settings";
|
|
|
|
import startupFile from "./mount/startup.lua";
|
|
|
|
import exprTemplate from "./mount/expr_template.lua";
|
2021-11-27 12:27:40 +00:00
|
|
|
import exampleNfp from "./mount/example.nfp";
|
|
|
|
import exampleNft from "./mount/example.nft";
|
2021-12-21 22:20:45 +00:00
|
|
|
import exampleAudioLicense from "./mount/example.dfpwm.LICENSE";
|
|
|
|
import exampleAudioUrl from "./mount/example.dfpwm";
|
2020-11-12 19:01:50 +00:00
|
|
|
|
|
|
|
const defaultFiles: { [filename: string]: string } = {
|
|
|
|
".settings": settingsFile,
|
|
|
|
"startup.lua": startupFile,
|
2020-11-20 21:59:17 +00:00
|
|
|
|
2021-11-27 12:27:40 +00:00
|
|
|
"data/example.nfp": exampleNfp,
|
|
|
|
"data/example.nft": exampleNft,
|
2020-11-12 19:01:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const clamp = (value: number, min: number, max: number): number => {
|
|
|
|
if (value < min) return min;
|
|
|
|
if (value > max) return max;
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2021-12-21 22:20:45 +00:00
|
|
|
const download = async (url: string): Promise<Uint8Array> => {
|
|
|
|
const result = await fetch(url);
|
|
|
|
if (result.status != 200) throw new Error(`${url} responded with ${result.status} ${result.statusText}`);
|
|
|
|
|
|
|
|
return new Uint8Array(await result.arrayBuffer());
|
|
|
|
};
|
|
|
|
|
|
|
|
let dfpwmAudio: Promise<Uint8Array> | null = null;
|
|
|
|
|
2020-11-12 19:01:50 +00:00
|
|
|
const Click = (options: { run: () => void }) =>
|
|
|
|
<button type="button" class="example-run" onClick={options.run}>Run ᐅ</button>
|
|
|
|
|
|
|
|
type WindowProps = {};
|
|
|
|
|
2021-12-21 00:55:13 +00:00
|
|
|
type Example = {
|
2021-12-21 22:20:45 +00:00
|
|
|
files: { [file: string]: string | Uint8Array },
|
|
|
|
peripheral: PeripheralKind | null,
|
2021-12-21 00:55:13 +00:00
|
|
|
}
|
2020-11-12 19:01:50 +00:00
|
|
|
|
2021-12-21 00:55:13 +00:00
|
|
|
type WindowState = {
|
2020-11-12 19:01:50 +00:00
|
|
|
exampleIdx: number,
|
2021-12-21 00:55:13 +00:00
|
|
|
} & ({ visible: false, example: null } | { visible: true, example: Example })
|
2020-11-12 19:01:50 +00:00
|
|
|
|
|
|
|
type Touch = { clientX: number, clientY: number };
|
|
|
|
|
|
|
|
class Window extends Component<WindowProps, WindowState> {
|
|
|
|
private positioned: boolean = false;
|
|
|
|
private left: number = 0;
|
|
|
|
private top: number = 0;
|
|
|
|
private dragging?: { downX: number, downY: number, initialX: number, initialY: number };
|
|
|
|
|
2021-12-21 00:55:13 +00:00
|
|
|
private snippets: { [file: string]: string } = {};
|
|
|
|
|
2020-11-12 19:01:50 +00:00
|
|
|
constructor(props: WindowProps, context: unknown) {
|
|
|
|
super(props, context);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
visible: false,
|
2021-12-21 00:55:13 +00:00
|
|
|
example: null,
|
2020-11-12 19:01:50 +00:00
|
|
|
exampleIdx: 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
const elements = document.querySelectorAll("pre[data-lua-kind]");
|
|
|
|
for (let i = 0; i < elements.length; i++) {
|
|
|
|
const element = elements[i] as HTMLElement;
|
|
|
|
|
|
|
|
let example = element.innerText;
|
2021-12-21 00:55:13 +00:00
|
|
|
|
|
|
|
const snippet = element.getAttribute("data-snippet");
|
|
|
|
if (snippet) this.snippets[snippet] = example;
|
|
|
|
|
2022-05-02 16:49:32 +00:00
|
|
|
// We attempt to pretty-print the result of a function _except_ when the function
|
|
|
|
// is print. This is pretty ugly, but prevents the confusing trailing "1".
|
|
|
|
if (element.getAttribute("data-lua-kind") == "expr" && !example.startsWith("print(")) {
|
2020-11-12 19:01:50 +00:00
|
|
|
example = exprTemplate.replace("__expr__", example);
|
|
|
|
}
|
2021-12-21 00:55:13 +00:00
|
|
|
|
|
|
|
const mount = element.getAttribute("data-mount");
|
2021-12-21 22:20:45 +00:00
|
|
|
const peripheral = element.getAttribute("data-peripheral");
|
|
|
|
render(<Click run={this.runExample(example, mount, peripheral)} />, element);
|
2020-11-12 19:01:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate(_: WindowProps, { visible }: WindowState) {
|
|
|
|
if (!visible && this.state.visible) this.setPosition(this.left, this.top);
|
|
|
|
}
|
|
|
|
|
|
|
|
public render(_: WindowProps, { visible, example, exampleIdx }: WindowState): ComponentChild {
|
|
|
|
return visible ? <div class="example-window" style={`transform: translate(${this.left}px, ${this.top}px);`}>
|
|
|
|
<div class="titlebar">
|
|
|
|
<div class="titlebar-drag" onMouseDown={this.onMouseDown} onTouchStart={this.onTouchDown} />
|
|
|
|
<button type="button" class="titlebar-close" onClick={this.close}>{"\u2715"}</button>
|
|
|
|
</div>
|
|
|
|
<div class="computer-container">
|
|
|
|
<Computer key={exampleIdx} files={{
|
2022-05-05 12:24:02 +00:00
|
|
|
...defaultFiles, ...example!.files,
|
2021-12-21 22:20:45 +00:00
|
|
|
}} peripherals={{ back: example!.peripheral }} />
|
2020-11-12 19:01:50 +00:00
|
|
|
</div>
|
|
|
|
</div> : <div class="example-window example-window-hidden" />;
|
|
|
|
}
|
|
|
|
|
2021-12-21 22:20:45 +00:00
|
|
|
private runExample(example: string, mount: string | null, peripheral: string | null): () => void {
|
|
|
|
return async () => {
|
2020-11-12 19:01:50 +00:00
|
|
|
if (!this.positioned) {
|
|
|
|
this.positioned = true;
|
|
|
|
this.left = 20;
|
|
|
|
this.top = 20;
|
|
|
|
}
|
|
|
|
|
2021-12-21 22:20:45 +00:00
|
|
|
const files: { [file: string]: string | Uint8Array } = { "example.lua": example };
|
2021-12-21 00:55:13 +00:00
|
|
|
if (mount !== null) {
|
|
|
|
for (const toMount of mount.split(",")) {
|
|
|
|
const [name, path] = toMount.split(":", 2);
|
|
|
|
files[path] = this.snippets[name] || "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-21 22:20:45 +00:00
|
|
|
if (example.includes("data/example.dfpwm")) {
|
|
|
|
files["data/example.dfpwm.LICENSE"] = exampleAudioLicense;
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (dfpwmAudio === null) dfpwmAudio = download(exampleAudioUrl);
|
|
|
|
files["data/example.dfpwm"] = await dfpwmAudio;
|
|
|
|
} catch (e) {
|
|
|
|
console.error("Cannot download example dfpwm", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-12 19:01:50 +00:00
|
|
|
this.setState(({ exampleIdx }: WindowState) => ({
|
|
|
|
visible: true,
|
2021-12-21 22:20:45 +00:00
|
|
|
example: {
|
|
|
|
files,
|
|
|
|
peripheral: peripheral as PeripheralKind | null,
|
|
|
|
},
|
2020-11-12 19:01:50 +00:00
|
|
|
exampleIdx: exampleIdx + 1,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private readonly close = () => this.setState({ visible: false });
|
|
|
|
|
|
|
|
// All the dragging code is terrible. However, I've had massive performance
|
|
|
|
// issues doing it other ways, so this'll have to do.
|
|
|
|
private onDown(e: Event, touch: Touch) {
|
|
|
|
e.stopPropagation();
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
this.dragging = {
|
|
|
|
initialX: this.left, initialY: this.top,
|
|
|
|
downX: touch.clientX, downY: touch.clientY
|
|
|
|
};
|
|
|
|
|
|
|
|
window.addEventListener("mousemove", this.onMouseDrag, true);
|
|
|
|
window.addEventListener("touchmove", this.onTouchDrag, true);
|
|
|
|
window.addEventListener("mouseup", this.onUp, true);
|
|
|
|
window.addEventListener("touchend", this.onUp, true);
|
|
|
|
}
|
|
|
|
private readonly onMouseDown = (e: MouseEvent) => this.onDown(e, e);
|
|
|
|
private readonly onTouchDown = (e: TouchEvent) => this.onDown(e, e.touches[0]);
|
|
|
|
|
|
|
|
private onDrag(e: Event, touch: Touch) {
|
|
|
|
e.stopPropagation();
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
const dragging = this.dragging;
|
|
|
|
if (!dragging) return;
|
|
|
|
|
|
|
|
this.setPosition(
|
|
|
|
dragging.initialX + (touch.clientX - dragging.downX),
|
|
|
|
dragging.initialY + (touch.clientY - dragging.downY),
|
|
|
|
);
|
|
|
|
};
|
|
|
|
private readonly onMouseDrag = (e: MouseEvent) => this.onDrag(e, e);
|
|
|
|
private readonly onTouchDrag = (e: TouchEvent) => this.onDrag(e, e.touches[0]);
|
|
|
|
|
|
|
|
private readonly onUp = (e: Event) => {
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
this.dragging = undefined;
|
|
|
|
|
|
|
|
window.removeEventListener("mousemove", this.onMouseDrag, true);
|
|
|
|
window.removeEventListener("touchmove", this.onTouchDrag, true);
|
|
|
|
window.removeEventListener("mouseup", this.onUp, true);
|
|
|
|
window.removeEventListener("touchend", this.onUp, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
private readonly setPosition = (left: number, top: number): void => {
|
|
|
|
const root = this.base as HTMLElement;
|
|
|
|
|
|
|
|
left = this.left = clamp(left, 0, window.innerWidth - root.offsetWidth);
|
|
|
|
top = this.top = clamp(top, 0, window.innerHeight - root.offsetHeight);
|
|
|
|
root.style.transform = `translate(${left}px, ${top}px)`;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const root = document.createElement("div");
|
|
|
|
document.body.appendChild(root);
|
|
|
|
render(<Window />, document.body, root);
|