device component with event to update state

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers
2024-04-07 22:07:12 -07:00
parent 5c7ff7c287
commit b609b2e94b
18 changed files with 656 additions and 153 deletions

View File

@@ -5,10 +5,10 @@ import "./nav";
import "./share";
import { ShareSessionDialog } from "./share";
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js';
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
// Set the base path to the folder you copied Shoelace's assets to
setBasePath('/shoelace');
setBasePath("/shoelace");
import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js";
@@ -18,6 +18,8 @@ import { Session } from "../session";
import { VirtualMachine } from "../virtual_machine";
import { openFile, saveFile } from "../utils";
import "../virtual_machine/ui";
@customElement("ic10emu-app")
export class App extends BaseElement {
static styles = [
@@ -34,11 +36,7 @@ export class App extends BaseElement {
}
.app-body {
flex-grow: 1;
// z-index: auto;
}
// .z-fix {
// z-index: 900;
// }
sl-split-panel {
height: 100%;
}
@@ -47,8 +45,8 @@ export class App extends BaseElement {
editorSettings: { fontSize: number; relativeLineNumbers: boolean };
@query('ace-ic10') accessor editor: IC10Editor;
@query('session-share-dialog') accessor shareDialog: ShareSessionDialog;
@query("ace-ic10") accessor editor: IC10Editor;
@query("session-share-dialog") accessor shareDialog: ShareSessionDialog;
// get editor() {
// return this.renderRoot.querySelector("ace-ic10") as IC10Editor;
@@ -62,14 +60,13 @@ export class App extends BaseElement {
window.App = this;
this.session = new Session();
this.vm = new VirtualMachine();
}
protected createRenderRoot(): HTMLElement | DocumentFragment {
const root = super.createRenderRoot();
root.addEventListener('app-share-session', this._handleShare.bind(this));
root.addEventListener('app-open-file', this._handleOpenFile.bind(this));
root.addEventListener('app-save-as', this._handleSaveAs.bind(this));
root.addEventListener("app-share-session", this._handleShare.bind(this));
root.addEventListener("app-open-file", this._handleOpenFile.bind(this));
root.addEventListener("app-save-as", this._handleSaveAs.bind(this));
return root;
}
@@ -85,7 +82,7 @@ export class App extends BaseElement {
snap-threshold="15"
>
<ace-ic10 slot="start" style=""></ace-ic10>
<div slot="end">Controls</div>
<div slot="end"><vm-ui></vm-ui></div>
</sl-split-panel>
</div>
<session-share-dialog></session-share-dialog>
@@ -93,8 +90,7 @@ export class App extends BaseElement {
`;
}
firstUpdated(): void {
}
firstUpdated(): void {}
_handleShare(_e: Event) {
// TODO:
@@ -109,7 +105,6 @@ export class App extends BaseElement {
_handleOpenFile(_e: Event) {
openFile(window.Editor.editor);
}
}
declare global {

View File

@@ -29,6 +29,14 @@ export const defaultCss = [
.mt-auto {
margin-top: auto !important;
}
.hstack {
display: flex;
flex-direction: row;
}
.vstack {
display: flex;
flex-direction: column;
}
`,
];

View File

@@ -20,6 +20,7 @@ export async function setupLspWorker() {
return worker;
}
export import Ace = ace.Ace;
export import EditSession = ace.Ace.EditSession;
export import Editor = ace.Ace.Editor;
import { Range } from "ace-builds";

View File

@@ -1,5 +1,6 @@
import {
ace,
Ace,
Editor,
EditSession,
Range,
@@ -32,8 +33,8 @@ import { html } from "lit";
import { Ref, createRef, ref } from "lit/directives/ref.js";
import { customElement, property, query } from "lit/decorators.js";
import { editorStyles } from "./styles";
import "./shortcuts-ui";
import { AceKeyboardShortcuts } from "./shortcuts-ui";
import "./shortcuts_ui";
import { AceKeyboardShortcuts } from "./shortcuts_ui";
@customElement("ace-ic10")
export class IC10Editor extends BaseElement {
@@ -71,9 +72,9 @@ export class IC10Editor extends BaseElement {
stylesAdded: string[];
tooltipObserver: MutationObserver;
@query('.e-kb-shortcuts') accessor kbShortcuts: AceKeyboardShortcuts;
@query(".e-kb-shortcuts") accessor kbShortcuts: AceKeyboardShortcuts;
@query('.e-settings-dialog') accessor settingDialog: SlDialog;
@query(".e-settings-dialog") accessor settingDialog: SlDialog;
constructor() {
super();
@@ -294,31 +295,30 @@ export class IC10Editor extends BaseElement {
window.App!.session.loadFromFragment();
window.App!.session.onActiveLine(((e: CustomEvent) => {
const session = e.detail;
for (const id of session.programs.keys()) {
const active_line = session.getActiveLine(id);
if (typeof active_line !== "undefined") {
const marker = that.active_line_markers.get(id);
if (marker) {
that.sessions.get(id)?.removeMarker(marker);
that.active_line_markers.set(id, null);
}
const session = that.sessions.get(id);
if (session) {
that.active_line_markers.set(
id,
session.addMarker(
new Range(active_line, 0, active_line, 1),
"vm_ic_active_line",
"fullLine",
true,
),
);
if (that.active_session == id) {
// editor.resize(true);
// TODO: Scroll to line if vm was stepped
//that.editor.scrollToLine(active_line, true, true, ()=>{})
}
const session = window.App?.session!;
const id = e.detail;
const active_line = session.getActiveLine(id);
if (typeof active_line !== "undefined") {
const marker = that.active_line_markers.get(id);
if (marker) {
that.sessions.get(id)?.removeMarker(marker);
that.active_line_markers.set(id, null);
}
const session = that.sessions.get(id);
if (session) {
that.active_line_markers.set(
id,
session.addMarker(
new Range(active_line, 0, active_line, 1),
"vm_ic_active_line",
"fullLine",
true,
),
);
if (that.active_session == id) {
// editor.resize(true);
// TODO: Scroll to line if vm was stepped
//that.editor.scrollToLine(active_line, true, true, ()=>{})
}
}
}

View File

@@ -316,6 +316,11 @@ export const editorStyles = css`
background: rgba(76, 87, 103, 0.19);
}
.vm_ic_active_line {
position: absolute;
background: rgba(121, 82, 179, 0.4);
z-index: 20;
}
/* ----------------------
* Editor Setting dialog
* ---------------------- */

View File

@@ -60,17 +60,21 @@ j ra
`;
import type { ICError } from "ic10emu_wasm";
export class Session extends EventTarget {
_programs: Map<number, string>;
_activeSession: number;
_errors: Map<number, ICError[]>;
_activeIC: number;
_activeLines: Map<number, number>;
_activeLine: number;
_save_timeout?: ReturnType<typeof setTimeout>;
constructor() {
super();
this._programs = new Map();
this._errors = new Map();
this._save_timeout = undefined;
this._activeSession = 0;
this._activeIC = 0;
this._activeLines = new Map();
this.loadFromFragment();
@@ -86,10 +90,26 @@ export class Session extends EventTarget {
set programs(programs) {
this._programs = new Map([...programs]);
this._fireOnLoad();
}
get activeSession() {
return this._activeSession;
get activeIC() {
return this._activeIC;
}
set activeIC(val: number) {
this._activeIC = val;
this.dispatchEvent(
new CustomEvent("session-active-ic", { detail: this.activeIC }),
);
}
onActiveIc(callback: EventListenerOrEventListenerObject) {
this.addEventListener("session-active-ic", callback);
}
get errors() {
return this._errors;
}
getActiveLine(id: number) {
@@ -98,7 +118,7 @@ export class Session extends EventTarget {
setActiveLine(id: number, line: number) {
this._activeLines.set(id, line);
this._fireOnActiveLine();
this._fireOnActiveLine(id);
}
set activeLine(line: number) {
@@ -110,6 +130,23 @@ export class Session extends EventTarget {
this.save();
}
setProgramErrors(id: number, errors: ICError[]) {
this._errors.set(id, errors);
this._fireOnErrors([id]);
}
_fireOnErrors(ids: number[]) {
this.dispatchEvent(
new CustomEvent("session-errors", {
detail: ids,
}),
);
}
onErrors(callback: EventListenerOrEventListenerObject) {
this.addEventListener("session-errors", callback);
}
onLoad(callback: EventListenerOrEventListenerObject) {
this.addEventListener("session-load", callback);
}
@@ -126,10 +163,10 @@ export class Session extends EventTarget {
this.addEventListener("active-line", callback);
}
_fireOnActiveLine() {
_fireOnActiveLine(id: number) {
this.dispatchEvent(
new CustomEvent("activeLine", {
detail: this,
new CustomEvent("active-line", {
detail: id,
}),
);
}
@@ -269,4 +306,3 @@ async function decompress(bytes: ArrayBuffer) {
}
return await concatUintArrays(chunks);
}

View File

@@ -1,6 +1,6 @@
import { Ace } from "ace-builds";
function docReady(fn: () => void) {
export function docReady(fn: () => void) {
// see if DOM is already available
if (
document.readyState === "complete" ||
@@ -12,8 +12,44 @@ function docReady(fn: () => void) {
}
}
function replacer(key: any, value: any) {
if(value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else {
return value;
}
}
function reviver(_key: any, value: any) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
}
}
return value;
}
export function toJson(value: any): string {
return JSON.stringify(value, replacer);
}
export function fromJson(value: string): any {
return JSON.parse(value, reviver)
}
export function structuralEqual(a: any, b: any): boolean {
const _a = JSON.stringify(a, replacer);
const _b = JSON.stringify(b, replacer);
return _a === _b;
}
// probably not needed, fetch() exists now
function makeRequest(opts: {
export function makeRequest(opts: {
method: string;
url: string;
headers: { [key: string]: string };
@@ -57,7 +93,7 @@ function makeRequest(opts: {
});
}
async function saveFile(content: BlobPart) {
export async function saveFile(content: BlobPart) {
const blob = new Blob([content], { type: "text/plain" });
if (typeof window.showSaveFilePicker !== "undefined") {
console.log("Saving via FileSystem API");
@@ -88,7 +124,7 @@ async function saveFile(content: BlobPart) {
}
}
async function openFile(editor: Ace.Editor) {
export async function openFile(editor: Ace.Editor) {
if (typeof window.showOpenFilePicker !== "undefined") {
console.log("opening file via FileSystem Api");
try {
@@ -121,4 +157,3 @@ async function openFile(editor: Ace.Editor) {
input.click();
}
}
export { docReady, makeRequest, saveFile, openFile };

View File

@@ -0,0 +1,78 @@
import { property, state } from "lit/decorators.js";
import { BaseElement } from "../components";
import {
DeviceRef,
Fields,
Reagents,
Slot,
Connection,
} from "ic10emu_wasm";
import { structuralEqual } from "../utils";
export class VMBaseDevice extends BaseElement {
@property({ type: Number }) accessor deviceID: number;
@state() protected accessor device: DeviceRef;
@state() accessor name: string | null;
@state() accessor nameHash: number | null;
@state() accessor prefabName: string | null;
@state() accessor fields: Fields;
@state() accessor slots: Slot[];
@state() accessor reagents: Reagents;
@state() accessor connections: Connection[];
constructor() {
super();
this.name = null;
this.nameHash = null;
}
connectedCallback(): void {
const root = super.connectedCallback();
this.device = window.VM!.devices.get(this.deviceID)!;
window.VM?.addEventListener(
"vm-device-modified",
this._handleDeviceModified.bind(this),
);
this.updateDevice();
return root;
}
_handleDeviceModified(e: CustomEvent) {
const id = e.detail;
if (this.deviceID === id) {
this.updateDevice();
}
}
updateDevice() {
const name = this.device.name ?? null;
if (this.name !== name) {
this.name = name;
}
const nameHash = this.device.nameHash ?? null;
if (this.nameHash !== nameHash) {
this.nameHash = nameHash;
}
const prefabName = this.device.prefabName ?? null;
if (this.prefabName !== prefabName) {
this.prefabName = prefabName;
}
const fields = this.device.fields;
if (!structuralEqual(this.fields, fields)) {
this.fields = fields;
}
const slots = this.device.slots;
if (!structuralEqual(this.slots, slots)) {
this.slots = slots;
}
const reagents = this.device.reagents;
if (!structuralEqual(this.reagents, reagents)) {
this.reagents = reagents;
}
const connections = this.device.connections;
if (!structuralEqual(this.connections, connections)) {
this.connections = connections;
}
}
}

View File

@@ -0,0 +1,190 @@
import { html, css } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { defaultCss } from "../components";
import { DeviceRef, ICError } from "ic10emu_wasm";
import { VMBaseDevice } from "./base_device";
import { structuralEqual } from "../utils";
import "@shoelace-style/shoelace/dist/components/card/card.js";
import "@shoelace-style/shoelace/dist/components/button-group/button-group.js";
import "@shoelace-style/shoelace/dist/components/button/button.js";
import "@shoelace-style/shoelace/dist/components/icon/icon.js";
import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js";
import "@shoelace-style/shoelace/dist/components/divider/divider.js";
@customElement("vm-ic-controls")
export class VMICControls extends VMBaseDevice {
@state() accessor icIP: number;
@state() accessor icOpCount: number;
@state() accessor icState: string;
@state() accessor errors: ICError[];
static styles = [
...defaultCss,
css`
:host {
}
.card {
margin-left: 1rem;
margin-right: 1rem;
margin-top: 1rem;
}
.controls {
display: flex;
flex-direction: row;
font-size: var(--sl-font-size-small);
}
.stats {
font-size: var(--sl-font-size-x-small);
}
.device-id {
margin-left: 2rem;
}
.button-group-toolbar sl-button-group:not(:last-of-type) {
margin-right: var(--sl-spacing-x-small);
}
sl-divider {
--spacing: 0.25rem;
}
`,
];
constructor() {
super();
this.deviceID = window.App!.session.activeIC;
}
connectedCallback(): void {
const root = super.connectedCallback();
window.VM?.addEventListener(
"vm-run-ic",
this._handleDeviceModified.bind(this),
);
window.App?.session.addEventListener(
"session-active-ic",
this._handleActiveIC.bind(this),
);
this.updateIC();
return root;
}
_handleActiveIC(e: CustomEvent) {
const id = e.detail;
if (this.deviceID !== id) {
this.deviceID = id;
this.device = window.VM!.devices.get(this.deviceID)!;
}
this.updateDevice();
}
updateIC() {
const ip = this.device.ip!;
if (this.icIP !== ip) {
this.icIP = ip;
}
const opCount = this.device.instructionCount!;
if (this.icOpCount !== opCount) {
this.icOpCount = opCount;
}
const state = this.device.state!;
if (this.icState !== state) {
this.icState = state;
}
const errors = this.device.program!.errors;
if (!structuralEqual(this.errors, errors)) {
this.errors = errors;
}
}
updateDevice(): void {
super.updateDevice();
this.updateIC();
}
protected firstUpdated(): void {}
protected render() {
return html`
<sl-card class="card">
<div class="controls" slot="header">
<sl-button-group>
<sl-tooltip
content="Run the active IC through one tick (128 operations)"
>
<sl-button size="small" variant="primary" @click=${this._handleRunClick}>
<span>Run</span>
<sl-icon name="play" label="Run" slot="prefix"></sl-icon>
</sl-button>
</sl-tooltip>
<sl-tooltip content="Run the active IC through a single operations">
<sl-button size="small" variant="success" @click=${this._handleStepClick}>
<span>Step</span>
<sl-icon
name="chevron-bar-right"
label="Step"
slot="prefix"
></sl-icon>
</sl-button>
</sl-tooltip>
<sl-tooltip content="Reset the active IC">
<sl-button size="small" variant="warning" @click=${this._handleResetClick}>
<span>Reset</span>
<sl-icon
name="arrow-clockwise"
label="Reset"
slot="prefix"
></sl-icon>
</sl-button>
</sl-tooltip>
</sl-button-group>
<div class="device-id">
Device:
${this.deviceID}${this.name ?? this.prefabName
? ` : ${this.name ?? this.prefabName}`
: ""}
</div>
</div>
<div class="stats">
<div class="hstack">
<span>Instruction Pointer</span>
<span class="ms-auto">${this.icIP}</span>
</div>
<sl-divider></sl-divider>
<div class="hstack">
<span>Last Run Operations Count</span>
<span class="ms-auto">${this.icOpCount}</span>
</div>
<sl-divider></sl-divider>
<div class="hstack">
<span>Last State</span>
<span class="ms-auto">${this.icState}</span>
</div>
<sl-divider></sl-divider>
<div class="vstack">
<span>Errors</span>
${this.errors.map(
(err) =>
html`<div class="hstack">
<span>
Line: ${err.ParseError.line} -
${err.ParseError.start}:${err.ParseError.end}
</span>
<span class="ms-auto">${err.ParseError.msg}</span>
</div>`,
)}
</div>
</div>
</sl-card>
`;
}
_handleRunClick() {
window.VM?.run();
}
_handleStepClick() {
window.VM?.step()
}
_handleResetClick() {
window.VM?.reset()
}
}

View File

@@ -1,7 +1,5 @@
import { DeviceRef, VM, init } from "ic10emu_wasm";
import { VMDeviceUI } from "./device";
import { BaseElement } from "../components";
// import { Card } from 'bootstrap';
import "./base_device";
declare global {
interface Window {
@@ -26,7 +24,7 @@ type DeviceDB = {
};
};
class VirtualMachine {
class VirtualMachine extends EventTarget {
ic10vm: VM;
ui: VirtualMachineUI;
_devices: Map<number, DeviceRef>;
@@ -34,12 +32,12 @@ class VirtualMachine {
db: DeviceDB;
constructor() {
super();
const vm = init();
window.VM = this;
this.ic10vm = vm;
// this.ui = new VirtualMachineUI(this);
this._devices = new Map();
this._ics = new Map();
@@ -58,7 +56,7 @@ class VirtualMachine {
}
get activeIC() {
return this._ics.get(window.App!.session.activeSession);
return this._ics.get(window.App!.session.activeIC);
}
updateDevices() {
@@ -98,7 +96,12 @@ class VirtualMachine {
if (ic && prog) {
console.time(`CompileProgram_${id}_${attempt}`);
try {
this.ics.get(id)!.setCode(progs.get(id)!);
this.ics.get(id)!.setCodeInvalid(progs.get(id)!);
const compiled = this.ics.get(id)?.program!;
window.App?.session.setProgramErrors(id, compiled.errors);
this.dispatchEvent(
new CustomEvent("vm-device-modified", { detail: id }),
);
} catch (e) {
console.log(e);
}
@@ -117,6 +120,9 @@ class VirtualMachine {
console.log(e);
}
this.update();
this.dispatchEvent(
new CustomEvent("vm-run-ic", { detail: this.activeIC!.id }),
);
}
}
@@ -129,6 +135,9 @@ class VirtualMachine {
console.log(e);
}
this.update();
this.dispatchEvent(
new CustomEvent("vm-run-ic", { detail: this.activeIC!.id }),
);
}
}
@@ -142,24 +151,36 @@ class VirtualMachine {
update() {
this.updateDevices();
this.ic10vm.lastOperationModified.forEach((id, _index, _modifiedIds) => {
if (this.devices.has(id)) {
this.dispatchEvent(
new CustomEvent("vm-device-modified", { detail: id }),
);
}
}, this);
const ic = this.activeIC!;
window.App!.session.setActiveLine(window.App!.session.activeSession, ic.ip!);
// this.ui.update(ic);
window.App!.session.setActiveLine(window.App!.session.activeIC, ic.ip!);
}
setRegister(index: number, val: number) {
const ic = this.activeIC!;
try {
ic.setRegister(index, val);
this.dispatchEvent(
new CustomEvent("vm-device-modified", { detail: ic.id }),
);
} catch (e) {
console.log(e);
}
}
setStack(addr: number, val: number) {
const ic = this.activeIC;
const ic = this.activeIC!;
try {
ic!.setStack(addr, val);
this.dispatchEvent(
new CustomEvent("vm-device-modified", { detail: ic.id }),
);
} catch (e) {
console.log(e);
}
@@ -176,14 +197,12 @@ class VirtualMachineUI {
state: VMStateUI;
registers: VMRegistersUI;
stack: VMStackUI;
devices: VMDeviceUI;
constructor(vm: VirtualMachine) {
this.vm = vm;
this.state = new VMStateUI(this);
this.registers = new VMRegistersUI(this);
this.stack = new VMStackUI(this);
this.devices = new VMDeviceUI(this);
const that = this;
@@ -214,7 +233,6 @@ class VirtualMachineUI {
this.state.update(ic);
this.registers.update(ic);
this.stack.update(ic);
this.devices.update(ic);
}
}

View File

@@ -0,0 +1,17 @@
import { HTMLTemplateResult, html, css } from "lit";
import { customElement, property, query } from "lit/decorators.js";
import { BaseElement, defaultCss } from "../components";
import "./controls.ts";
@customElement("vm-ui")
export class VMUI extends BaseElement {
constructor() {
super();
}
protected render() {
return html`<vm-ic-controls>`;
}
}