1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-24 06:03:28 +00:00
CC-Tweaked/src/web/index.tsx
Jonathan Coates a4c9e89370
Runnable examples (#576)
Provides a basic interface for running examples on tweaked.cc. This is probably
janky as anything, but it works on my machine.

This is the culmination of 18 months of me building far too much infrastructure
(copy-cat, illuaminate), so that's nice I guess.

I should probably get out more.
2020-11-12 19:01:50 +00:00

156 lines
5.3 KiB
TypeScript

import { render, h, Component, Computer } from "copycat/embed";
import type { ComponentChild } from "preact";
import settingsFile from "./mount/.settings";
import startupFile from "./mount/startup.lua";
import exprTemplate from "./mount/expr_template.lua";
const defaultFiles: { [filename: string]: string } = {
".settings": settingsFile,
"startup.lua": startupFile,
};
const clamp = (value: number, min: number, max: number): number => {
if (value < min) return min;
if (value > max) return max;
return value;
}
const Click = (options: { run: () => void }) =>
<button type="button" class="example-run" onClick={options.run}>Run </button>
type WindowProps = {};
type WindowState = {
visible: boolean,
example: string,
exampleIdx: number,
}
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 };
constructor(props: WindowProps, context: unknown) {
super(props, context);
this.state = {
visible: false,
example: "",
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;
if (element.getAttribute("data-lua-kind") == "expr") {
example = exprTemplate.replace("__expr__", example);
}
render(<Click run={this.runExample(example)} />, element);
}
}
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={{
"example.lua": example, ...defaultFiles
}} />
</div>
</div> : <div class="example-window example-window-hidden" />;
}
private runExample(example: string): () => void {
return () => {
if (!this.positioned) {
this.positioned = true;
this.left = 20;
this.top = 20;
}
this.setState(({ exampleIdx }: WindowState) => ({
visible: true,
example: example,
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);