Device Cards
brings the rework inline were last efforts left off Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
@@ -149,7 +149,6 @@ export class VMActiveIC extends VMBaseDevice {
|
||||
"session-active-ic",
|
||||
this._handleActiveIC.bind(this),
|
||||
);
|
||||
this.updateIC();
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ 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";
|
||||
import "@shoelace-style/shoelace/dist/components/select/select.js";
|
||||
import "@shoelace-style/shoelace/dist/components/badge/badge.js";
|
||||
import "@shoelace-style/shoelace/dist/components/option/option.js";
|
||||
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js";
|
||||
|
||||
@customElement("vm-ic-controls")
|
||||
export class VMICControls extends VMActiveIC {
|
||||
@@ -41,6 +45,19 @@ export class VMICControls extends VMActiveIC {
|
||||
sl-divider {
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
sl-button[variant="success"] {
|
||||
/* Changes the success theme color to purple using primitives */
|
||||
--sl-color-success-600: var(--sl-color-purple-700);
|
||||
}
|
||||
sl-button[variant="primary"] {
|
||||
/* Changes the success theme color to purple using primitives */
|
||||
--sl-color-primary-600: var(--sl-color-cyan-600);
|
||||
}
|
||||
sl-button[variant="warning"] {
|
||||
/* Changes the success theme color to purple using primitives */
|
||||
--sl-color-warning-600: var(--sl-color-amber-600);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -50,6 +67,7 @@ export class VMICControls extends VMActiveIC {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const ics = Array.from(window.VM!.ics);
|
||||
return html`
|
||||
<sl-card class="card">
|
||||
<div class="controls" slot="header">
|
||||
@@ -96,10 +114,19 @@ export class VMICControls extends VMActiveIC {
|
||||
</sl-tooltip>
|
||||
</sl-button-group>
|
||||
<div class="device-id">
|
||||
Device:
|
||||
${this.deviceID}${this.name ?? this.prefabName
|
||||
? ` : ${this.name ?? this.prefabName}`
|
||||
: ""}
|
||||
<sl-select
|
||||
hoist
|
||||
placement="bottom"
|
||||
value="${this.deviceID}"
|
||||
@sl-change=${this._handleChangeActiveIC}
|
||||
>
|
||||
${ics.map(
|
||||
([id, device], _index) =>
|
||||
html`<sl-option value=${id}>
|
||||
Device:${id} ${device.name ?? device.prefabName}
|
||||
</sl-option>`,
|
||||
)}
|
||||
</sl-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
@@ -121,15 +148,15 @@ export class VMICControls extends VMActiveIC {
|
||||
<div class="vstack">
|
||||
<span>Errors</span>
|
||||
${this.errors.map(
|
||||
(err) =>
|
||||
html`<div class="hstack">
|
||||
(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>
|
||||
@@ -145,4 +172,10 @@ export class VMICControls extends VMActiveIC {
|
||||
_handleResetClick() {
|
||||
window.VM?.reset();
|
||||
}
|
||||
|
||||
_handleChangeActiveIC(e: CustomEvent) {
|
||||
const select = e.target as SlSelect;
|
||||
const icId = parseInt(select.value as string);
|
||||
window.App!.session.activeIC = icId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,398 +1,359 @@
|
||||
import { Offcanvas } from "bootstrap";
|
||||
import { VirtualMachine, VirtualMachineUI } from ".";
|
||||
import { DeviceRef, VM } from "ic10emu_wasm";
|
||||
import { Slot } from "ic10emu_wasm";
|
||||
import { html, css, HTMLTemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { BaseElement, defaultCss } from "../components";
|
||||
import { VMBaseDevice } from "./base_device";
|
||||
|
||||
import "@shoelace-style/shoelace/dist/components/card/card.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/input/input.js";
|
||||
import "@shoelace-style/shoelace/dist/components/details/details.js";
|
||||
import "@shoelace-style/shoelace/dist/components/tab/tab.js";
|
||||
import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js";
|
||||
import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js";
|
||||
import "@shoelace-style/shoelace/dist/components/copy-button/copy-button.js";
|
||||
import "@shoelace-style/shoelace/dist/components/select/select.js";
|
||||
import "@shoelace-style/shoelace/dist/components/badge/badge.js";
|
||||
import "@shoelace-style/shoelace/dist/components/option/option.js";
|
||||
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
|
||||
import { parseNumber, structuralEqual } from "../utils";
|
||||
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js";
|
||||
|
||||
|
||||
|
||||
class VMDeviceUI {
|
||||
ui: VirtualMachineUI;
|
||||
summary: HTMLDivElement;
|
||||
canvasEl: HTMLDivElement;
|
||||
deviceCountEl: HTMLElement;
|
||||
canvas: Offcanvas;
|
||||
private _deviceSummaryCards: Map<number, VMDeviceSummaryCard>;
|
||||
private _offCanvaseCards: Map<
|
||||
number,
|
||||
{ col: HTMLElement; card: VMDeviceCard }
|
||||
>;
|
||||
|
||||
constructor(ui: VirtualMachineUI) {
|
||||
const that = this;
|
||||
that.ui = ui;
|
||||
this.summary = document.getElementById("vmDeviceSummary") as HTMLDivElement;
|
||||
this.canvasEl = document.getElementById(
|
||||
"vmDevicesOCBody",
|
||||
) as HTMLDivElement;
|
||||
this.deviceCountEl = document.getElementById("vmViewDeviceCount");
|
||||
this.canvas = new Offcanvas(this.canvasEl);
|
||||
this._deviceSummaryCards = new Map();
|
||||
this._offCanvaseCards = new Map();
|
||||
}
|
||||
|
||||
update(active_ic: DeviceRef) {
|
||||
const devices = window.VM.devices;
|
||||
this.deviceCountEl.textContent = `(${devices.size})`;
|
||||
for (const [id, device] of devices) {
|
||||
if (!this._deviceSummaryCards.has(id)) {
|
||||
this._deviceSummaryCards.set(id, new VMDeviceSummaryCard(this, device));
|
||||
}
|
||||
if (!this._offCanvaseCards.has(id)) {
|
||||
const col = document.createElement("div");
|
||||
col.classList.add("col");
|
||||
col.id = `${this.canvasEl.id}_col${id}`
|
||||
this.canvasEl.appendChild(col);
|
||||
this._offCanvaseCards.set(id, {
|
||||
col,
|
||||
card: new VMDeviceCard(this, col, device),
|
||||
});
|
||||
}
|
||||
}
|
||||
this._deviceSummaryCards.forEach((card, id, cards) => {
|
||||
if (!devices.has(id)) {
|
||||
card.destroy();
|
||||
cards.delete(id);
|
||||
} else {
|
||||
card.update(active_ic);
|
||||
}
|
||||
}, this);
|
||||
this._offCanvaseCards.forEach((card, id, cards) => {
|
||||
if (!devices.has(id)) {
|
||||
card.card.destroy();
|
||||
card.col.remove();
|
||||
cards.delete(id);
|
||||
} else {
|
||||
card.card.update(active_ic);
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
class VMDeviceSummaryCard {
|
||||
root: HTMLDivElement;
|
||||
viewBtn: HTMLButtonElement;
|
||||
deviceUI: VMDeviceUI;
|
||||
device: DeviceRef;
|
||||
badges: HTMLSpanElement[];
|
||||
constructor(deviceUI: VMDeviceUI, device: DeviceRef) {
|
||||
// const that = this;
|
||||
this.deviceUI = deviceUI;
|
||||
this.device = device;
|
||||
this.root = document.createElement("div");
|
||||
this.root.classList.add(
|
||||
"hstack",
|
||||
"gap-2",
|
||||
"bg-light-subtle",
|
||||
"border",
|
||||
"border-secondary-subtle",
|
||||
"rounded",
|
||||
);
|
||||
this.viewBtn = document.createElement("button");
|
||||
this.viewBtn.type = "button";
|
||||
this.viewBtn.classList.add("btn", "btn-success");
|
||||
this.root.appendChild(this.viewBtn);
|
||||
this.deviceUI.summary.appendChild(this.root);
|
||||
this.badges = [];
|
||||
|
||||
this.update(window.VM.activeIC);
|
||||
}
|
||||
|
||||
update(active_ic: DeviceRef) {
|
||||
const that = this;
|
||||
// clear previous badges
|
||||
this.badges.forEach((badge) => badge.remove());
|
||||
this.badges = [];
|
||||
|
||||
//update name
|
||||
var deviceName = this.device.name ?? this.device.prefabName ?? "";
|
||||
if (deviceName) {
|
||||
deviceName = `: ${deviceName}`;
|
||||
}
|
||||
const btnTxt = `Device ${this.device.id}${deviceName}`;
|
||||
this.viewBtn.textContent = btnTxt;
|
||||
|
||||
// regenerate badges
|
||||
this.device.connections.forEach((conn, index) => {
|
||||
if (typeof conn === "object") {
|
||||
var badge = document.createElement("span");
|
||||
badge.classList.add("badge", "text-bg-light");
|
||||
badge.textContent = `Net ${index}:${conn.CableNetwork}`;
|
||||
that.badges.push(badge);
|
||||
that.root.appendChild(badge);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.device.id === active_ic.id) {
|
||||
var badge = document.createElement("span");
|
||||
badge.classList.add("badge", "text-bg-success");
|
||||
badge.textContent = "db";
|
||||
that.badges.push(badge);
|
||||
that.root.appendChild(badge);
|
||||
}
|
||||
|
||||
active_ic.pins?.forEach((id, index) => {
|
||||
if (that.device.id === id) {
|
||||
var badge = document.createElement("span");
|
||||
badge.classList.add("badge", "text-bg-success");
|
||||
badge.textContent = `d${index}`;
|
||||
that.badges.push(badge);
|
||||
that.root.appendChild(badge);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.root.remove();
|
||||
}
|
||||
}
|
||||
|
||||
class VMDeviceCard {
|
||||
ui: VMDeviceUI;
|
||||
container: HTMLElement;
|
||||
device: DeviceRef;
|
||||
root: HTMLDivElement;
|
||||
nav: HTMLUListElement;
|
||||
|
||||
header: HTMLDivElement;
|
||||
nameInput: HTMLInputElement;
|
||||
nameHash: HTMLSpanElement;
|
||||
body: HTMLDivElement;
|
||||
badges: HTMLSpanElement[];
|
||||
fieldsContainer: HTMLDivElement;
|
||||
slotsContainer: HTMLDivElement;
|
||||
pinsContainer: HTMLDivElement;
|
||||
networksContainer: HTMLDivElement;
|
||||
reagentsContainer: HTMLDivElement;
|
||||
nav_id: string;
|
||||
navTabs: { [key: string]: { li: HTMLLIElement; button: HTMLButtonElement } };
|
||||
paneContainer: HTMLDivElement;
|
||||
tabPanes: { [key: string]: HTMLElement };
|
||||
image: HTMLImageElement;
|
||||
@customElement("vm-device-card")
|
||||
export class VMDeviceCard extends VMBaseDevice {
|
||||
image_err: boolean;
|
||||
title: HTMLHeadingElement;
|
||||
fieldEls: Map<string, VMDeviceField>;
|
||||
|
||||
constructor(ui: VMDeviceUI, container: HTMLElement, device: DeviceRef) {
|
||||
this.ui = ui;
|
||||
this.container = container;
|
||||
this.device = device;
|
||||
this.nav_id = `${this.container.id}_vmDeviceCard${this.device.id}`;
|
||||
|
||||
this.root = document.createElement("div");
|
||||
this.root.classList.add("card");
|
||||
|
||||
this.header = document.createElement("div");
|
||||
this.header.classList.add("card-header", "hstack");
|
||||
this.image = document.createElement("img");
|
||||
this.image_err = false;
|
||||
this.image.src = `/img/stationpedia/${this.device.prefabName}.png`;
|
||||
this.image.onerror = this.onImageErr;
|
||||
this.image.width = 48;
|
||||
this.image.classList.add("me-2");
|
||||
this.header.appendChild(this.image);
|
||||
|
||||
this.title = document.createElement("h5");
|
||||
this.title.textContent = `Device ${this.device.id} : ${this.device.prefabName ?? ""}`;
|
||||
this.header.appendChild(this.title);
|
||||
|
||||
this.nameInput = document.createElement("input");
|
||||
this.nameHash = document.createElement("span");
|
||||
|
||||
this.root.appendChild(this.header);
|
||||
|
||||
this.body = document.createElement("div");
|
||||
this.body.classList.add("card-body");
|
||||
this.root.appendChild(this.body);
|
||||
|
||||
this.nav = document.createElement("ul");
|
||||
this.nav.classList.add("nav", "nav-tabs");
|
||||
this.nav.role = "tablist";
|
||||
this.nav.id = this.nav_id;
|
||||
this.navTabs = {};
|
||||
this.tabPanes = {};
|
||||
|
||||
this.body.appendChild(this.nav);
|
||||
|
||||
this.paneContainer = document.createElement("div");
|
||||
this.paneContainer.id = `${this.nav_id}_tabs`;
|
||||
|
||||
this.body.appendChild(this.paneContainer);
|
||||
|
||||
this.badges = [];
|
||||
this.fieldsContainer = document.createElement("div");
|
||||
this.fieldsContainer.id = `${this.nav_id}_fields`;
|
||||
this.fieldsContainer.classList.add("vstack");
|
||||
this.fieldEls = new Map();
|
||||
this.slotsContainer = document.createElement("div");
|
||||
this.slotsContainer.id = `${this.nav_id}_slots`;
|
||||
this.slotsContainer.classList.add("vstack");
|
||||
this.reagentsContainer = document.createElement("div");
|
||||
this.reagentsContainer.id = `${this.nav_id}_reagents`;
|
||||
this.reagentsContainer.classList.add("vstack");
|
||||
this.networksContainer = document.createElement("div");
|
||||
this.networksContainer.id = `${this.nav_id}_networks`;
|
||||
this.networksContainer.classList.add("vstack");
|
||||
this.pinsContainer = document.createElement("div");
|
||||
this.pinsContainer.id = `${this.nav_id}_pins`;
|
||||
this.pinsContainer.classList.add("vstack");
|
||||
|
||||
this.addTab("Fields", this.fieldsContainer);
|
||||
this.addTab("Slots", this.slotsContainer);
|
||||
this.addTab("Networks", this.networksContainer);
|
||||
|
||||
this.update(window.VM.activeIC);
|
||||
|
||||
// do last to minimise reflows
|
||||
this.container.appendChild(this.root);
|
||||
}
|
||||
static styles = [
|
||||
...defaultCss,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.card {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.image {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.header-name {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
// .device-name {
|
||||
// box-sizing: border-box;
|
||||
// width: 8rem;
|
||||
// }
|
||||
// .device-name-hash {
|
||||
// box-sizing: border-box;
|
||||
// width: 5rem;
|
||||
// }
|
||||
sl-divider {
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
sl-button[variant="success"] {
|
||||
/* Changes the success theme color to purple using primitives */
|
||||
--sl-color-success-600: var(--sl-color-purple-700);
|
||||
}
|
||||
sl-button[variant="primary"] {
|
||||
/* Changes the success theme color to purple using primitives */
|
||||
--sl-color-primary-600: var(--sl-color-cyan-600);
|
||||
}
|
||||
sl-button[variant="warning"] {
|
||||
/* Changes the success theme color to purple using primitives */
|
||||
--sl-color-warning-600: var(--sl-color-amber-600);
|
||||
}
|
||||
sl-tab-group {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
--indicator-color: var(--sl-color-purple-600);
|
||||
--sl-color-primary-600: var(--sl-color-purple-600);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
onImageErr(e: Event) {
|
||||
this.image_err = true;
|
||||
console.log("Image load error", e);
|
||||
}
|
||||
|
||||
addNav(name: string, target: string) {
|
||||
if (!(name in this.navTabs)) {
|
||||
var li = document.createElement("li");
|
||||
li.classList.add("nav-item");
|
||||
li.role = "presentation";
|
||||
var button = document.createElement("button");
|
||||
button.classList.add("nav-link");
|
||||
if (!(Object.keys(this.navTabs).length > 0)) {
|
||||
button.classList.add("active");
|
||||
button.tabIndex = 0;
|
||||
} else {
|
||||
button.tabIndex = -1;
|
||||
}
|
||||
button.id = `${this.nav_id}_tab_${name}`;
|
||||
button.setAttribute("data-bs-toggle", "tab");
|
||||
button.setAttribute("data-bs-target", `#${target}`);
|
||||
button.type = "button";
|
||||
button.role = "tab";
|
||||
button.setAttribute("aria-controls", target);
|
||||
button.setAttribute(
|
||||
"aria-selected",
|
||||
Object.keys(this.navTabs).length > 0 ? "false" : "true",
|
||||
);
|
||||
button.textContent = name;
|
||||
li.appendChild(button);
|
||||
this.nav.appendChild(li);
|
||||
this.navTabs[name] = { li, button };
|
||||
return true;
|
||||
renderHeader(): HTMLTemplateResult {
|
||||
const activeIc = window.VM?.activeIC;
|
||||
const badges: HTMLTemplateResult[] = [];
|
||||
if (this.deviceID == activeIc?.id) {
|
||||
badges.push(html`<sl-badge variant="primary" pill pulse>db</sl-badge>`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeNav(name: string) {
|
||||
if (name in this.navTabs) {
|
||||
this.navTabs[name].li.remove();
|
||||
delete this.navTabs[name];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
addTab(name: string, tab: HTMLElement) {
|
||||
const paneName = `${this.nav_id}_pane_${name}`;
|
||||
if (this.addNav(name, paneName)) {
|
||||
if (name in this.tabPanes) {
|
||||
this.tabPanes[name].remove();
|
||||
}
|
||||
const pane = document.createElement("div");
|
||||
pane.classList.add("tap-pane", "fade");
|
||||
if (!(Object.keys(this.tabPanes).length > 0)) {
|
||||
pane.classList.add("show", "active");
|
||||
}
|
||||
pane.id = paneName;
|
||||
pane.role = "tabpanel";
|
||||
pane.setAttribute("aria-labelledby", `${this.nav_id}_tab_${name}`);
|
||||
pane.tabIndex = 0;
|
||||
|
||||
this.paneContainer.appendChild(pane);
|
||||
pane.appendChild(tab);
|
||||
this.tabPanes[name] = tab;
|
||||
}
|
||||
}
|
||||
|
||||
removeTab(name: string) {
|
||||
let result = this.removeNav(name);
|
||||
if (name in this.tabPanes) {
|
||||
this.tabPanes[name].remove();
|
||||
delete this.tabPanes[name];
|
||||
return true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
update(active_ic: DeviceRef) {
|
||||
if (this.device.pins) {
|
||||
this.addTab("Pins", this.pinsContainer);
|
||||
} else {
|
||||
this.removeTab("Pins");
|
||||
}
|
||||
|
||||
// fields
|
||||
for (const [name, _field] of this.device.fields) {
|
||||
if (!this.fieldEls.has(name)) {
|
||||
const field = new VMDeviceField(this.device, name, this, this.fieldsContainer);
|
||||
this.fieldEls.set(name, field);
|
||||
}
|
||||
}
|
||||
this.fieldEls.forEach((field, name, map) => {
|
||||
if(!this.device.fields.has(name)) {
|
||||
field.destroy();
|
||||
map.delete(name);
|
||||
} else {
|
||||
field.update(active_ic);
|
||||
activeIc?.pins?.forEach((id, index) => {
|
||||
if (this.deviceID == id) {
|
||||
badges.push(html`<sl-badge variant="success" pill></sl-badge>`);
|
||||
}
|
||||
}, this);
|
||||
|
||||
|
||||
// TODO Reagents
|
||||
return html`
|
||||
<img
|
||||
class="image"
|
||||
src="img/stationpedia/${this.prefabName}.png"
|
||||
@onerr=${this.onImageErr}
|
||||
/>
|
||||
<div class="header-name">
|
||||
<sl-input
|
||||
id="vmDeviceCard${this.deviceID}Name"
|
||||
class="device-name"
|
||||
size="small"
|
||||
pill
|
||||
placeholder="${this.prefabName}"
|
||||
@sl-change=${this._handleChangeName}
|
||||
>
|
||||
<span slot="prefix">Device ${this.deviceID}</span>
|
||||
<sl-copy-button
|
||||
slot="suffix"
|
||||
from="vmDeviceCard${this.deviceID}Name.value"
|
||||
></sl-copy-button>
|
||||
</sl-input>
|
||||
<sl-input
|
||||
id="vmDeviceCard${this.deviceID}NameHash"
|
||||
size="small"
|
||||
pill
|
||||
class="device-name-hash"
|
||||
value="${this.nameHash}"
|
||||
disabled
|
||||
>
|
||||
<span slot="prefix">Name Hash</span>
|
||||
<sl-copy-button
|
||||
slot="suffix"
|
||||
from="vmDeviceCard${this.deviceID}NameHash.value"
|
||||
></sl-copy-button>
|
||||
</sl-input>
|
||||
${badges.map((badge) => badge)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.root.remove();
|
||||
renderFields(): HTMLTemplateResult {
|
||||
const fields = Array.from(this.fields);
|
||||
return html`
|
||||
${fields.map(([name, field], _index, _fields) => {
|
||||
return html` <sl-input
|
||||
key="${name}"
|
||||
value="${field.value}"
|
||||
@sl-change=${this._handleChangeField}
|
||||
>
|
||||
<span slot="prefix">${name}</span>
|
||||
<span slot="suffix">${field.field_type}</span>
|
||||
</sl-input>`;
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult {
|
||||
const fields = Array.from(slot.fields);
|
||||
return html`
|
||||
<sl-card class="slot-card">
|
||||
<span slot="header" class="slot-header"
|
||||
>${slotIndex} : ${slot.typ}</span
|
||||
>
|
||||
<div class="slot-fields">
|
||||
${fields.map(
|
||||
([name, field], _index, _fields) => html`
|
||||
<sl-input
|
||||
slotIndex=${slotIndex}
|
||||
key="${name}"
|
||||
value="${field.value}"
|
||||
@sl-change=${this._handleChangeSlotField}
|
||||
>
|
||||
<span slot="prefix">${name}</span>
|
||||
<span slot="suffix">${field.field_type}</span>
|
||||
</sl-input>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</sl-card>
|
||||
`;
|
||||
}
|
||||
renderSlots(): HTMLTemplateResult {
|
||||
return html`
|
||||
<div clas="slots">
|
||||
${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
renderReagents(): HTMLTemplateResult {
|
||||
return html``;
|
||||
}
|
||||
renderNetworks(): HTMLTemplateResult {
|
||||
const vmNetworks = window.VM!.networks;
|
||||
return html`
|
||||
<div class="networks">
|
||||
${this.connections.map((connection, index, _conns) => {
|
||||
const conn =
|
||||
typeof connection === "object" ? connection.CableNetwork : null;
|
||||
return html`
|
||||
<sl-select
|
||||
hoist
|
||||
placement="top"
|
||||
clearable
|
||||
key=${index}
|
||||
value=${conn}
|
||||
?disabled=${conn === null}
|
||||
@sl-change=${this._handleChangeConnection}
|
||||
>
|
||||
<span slot="prefix">Connection:${index}</span>
|
||||
${vmNetworks.map(
|
||||
(net) =>
|
||||
html`<sl-option value=${net}>Network ${net}</sl-option>`,
|
||||
)}
|
||||
</sl-select>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
renderPins(): HTMLTemplateResult {
|
||||
const pins = this.pins;
|
||||
const visibleDevices = window.VM!.visibleDevices(this.deviceID);
|
||||
return html`
|
||||
<div class="pins">
|
||||
${pins?.map(
|
||||
(pin, index) =>
|
||||
html`<sl-select
|
||||
hoist
|
||||
placement="top"
|
||||
clearable
|
||||
key=${index}
|
||||
value=${pin}
|
||||
@sl-change=${this._handleChangePin}
|
||||
>
|
||||
<span slot="prefix">d${index}</span>
|
||||
${visibleDevices.map(
|
||||
(device, _index) =>
|
||||
html`<sl-option value=${device.id}>
|
||||
Device ${device.id} : ${device.name ?? device.prefabName}
|
||||
</sl-option>`,
|
||||
)}
|
||||
</sl-select>`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
protected render(): HTMLTemplateResult {
|
||||
return html`
|
||||
<sl-card class="card">
|
||||
<div class="header" slot="header">${this.renderHeader()}</div>
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel="fields">Fields</sl-tab>
|
||||
<sl-tab slot="nav" panel="slots">Slots</sl-tab>
|
||||
<sl-tab slot="nav" panel="reagents" disabled>Reagents</sl-tab>
|
||||
<sl-tab slot="nav" panel="networks">Networks</sl-tab>
|
||||
<sl-tab slot="nav" panel="pins" ?disabled=${!this.pins}>Pins</sl-tab>
|
||||
|
||||
<sl-tab-panel name="fields">${this.renderFields()}</sl-tab-panel>
|
||||
<sl-tab-panel name="slots">${this.renderSlots()}</sl-tab-panel>
|
||||
<sl-tab-panel name="reagents">${this.renderReagents()}</sl-tab-panel>
|
||||
<sl-tab-panel name="networks">${this.renderNetworks()}</sl-tab-panel>
|
||||
<sl-tab-panel name="pins">${this.renderPins()}</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
</sl-card>
|
||||
`;
|
||||
}
|
||||
|
||||
_handleChangeName(e: CustomEvent) {
|
||||
const input = e.target as SlInput;
|
||||
this.device.setName(input.value);
|
||||
this.updateDevice();
|
||||
}
|
||||
|
||||
_handleChangeField(e: CustomEvent) {
|
||||
const input = e.target as SlInput;
|
||||
const field = input.getAttribute("key")!;
|
||||
const val = parseNumber(input.value);
|
||||
this.device.setField(field, val);
|
||||
this.updateDevice();
|
||||
}
|
||||
|
||||
_handleChangeSlotField(e: CustomEvent) {
|
||||
const input = e.target as SlInput;
|
||||
const slot = parseInt(input.getAttribute("slotIndex")!);
|
||||
const field = input.getAttribute("key")!;
|
||||
const val = parseNumber(input.value);
|
||||
this.device.setSlotField(slot, field, val);
|
||||
this.updateDevice();
|
||||
}
|
||||
|
||||
_handleChangeConnection(e: CustomEvent) {
|
||||
const select = e.target as SlSelect;
|
||||
const conn = parseInt(select.getAttribute("key")!);
|
||||
const last = this.device.connections[conn];
|
||||
const val = select.value ? parseInt(select.value as string) : undefined;
|
||||
if (typeof last === "object" && typeof last.CableNetwork === "number") {
|
||||
// is there no other connection to the previous network?
|
||||
if (
|
||||
!this.device.connections.some((other_conn, index) => {
|
||||
structuralEqual(last, other_conn) && index !== conn;
|
||||
})
|
||||
) {
|
||||
this.device.removeDeviceFromNetwork(last.CableNetwork);
|
||||
}
|
||||
}
|
||||
if (typeof val !== "undefined") {
|
||||
this.device.addDeviceToNetwork(conn, val);
|
||||
} else {
|
||||
this.device.setConnection(conn, val);
|
||||
}
|
||||
|
||||
this.updateDevice();
|
||||
}
|
||||
|
||||
_handleChangePin(e: CustomEvent) {
|
||||
const select = e.target as SlSelect;
|
||||
const pin = parseInt(select.getAttribute("key")!);
|
||||
const val = select.value ? parseInt(select.value as string) : undefined;
|
||||
this.device.setPin(pin, val);
|
||||
this.updateDevice();
|
||||
}
|
||||
}
|
||||
|
||||
class VMDeviceField {
|
||||
container: HTMLElement;
|
||||
card: VMDeviceCard;
|
||||
device: DeviceRef;
|
||||
field: string;
|
||||
root: HTMLDivElement;
|
||||
name: HTMLSpanElement;
|
||||
fieldType: HTMLSpanElement;
|
||||
input: HTMLInputElement;
|
||||
constructor(device: DeviceRef, field: string, card: VMDeviceCard, container: HTMLElement) {
|
||||
this.device = device;
|
||||
this.field = field;
|
||||
this.card = card;
|
||||
this.container = container;
|
||||
this.root = document.createElement('div');
|
||||
this.root.classList.add("input-group", "input-group-sm");
|
||||
this.name = document.createElement('span');
|
||||
this.name.classList.add("input-group-text", "field_name");
|
||||
this.name.textContent = this.field;
|
||||
this.root.appendChild(this.name);
|
||||
this.fieldType = document.createElement('span');
|
||||
this.fieldType.classList.add("input-group-text", "field_type");
|
||||
this.fieldType.textContent = device.fields.get(this.field)?.field_type;
|
||||
this.root.appendChild(this.fieldType);
|
||||
this.input = document.createElement('input');
|
||||
this.input.type = "text";
|
||||
this.input.value = this.device.fields.get(this.field)?.value.toString();
|
||||
this.root.appendChild(this.input);
|
||||
@customElement("vm-device-list")
|
||||
export class VMDeviceList extends BaseElement {
|
||||
@state() accessor devices: number[];
|
||||
|
||||
this.container.appendChild(this.root);
|
||||
constructor() {
|
||||
super();
|
||||
this.devices = window.VM!.deviceIds;
|
||||
}
|
||||
destroy () {
|
||||
this.root.remove();
|
||||
|
||||
connectedCallback(): void {
|
||||
const root = super.connectedCallback();
|
||||
window.VM?.addEventListener(
|
||||
"vm-devices-update",
|
||||
this._handleDevicesUpdate.bind(this),
|
||||
);
|
||||
return root;
|
||||
}
|
||||
update(_active_ic: DeviceRef) {
|
||||
this.input.value = this.device.fields.get(this.field)?.value.toString();
|
||||
|
||||
_handleDevicesUpdate(e: CustomEvent) {
|
||||
const ids = e.detail;
|
||||
if (!structuralEqual(this.devices, ids)) {
|
||||
this.devices = ids;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): HTMLTemplateResult {
|
||||
return html`
|
||||
<div class="device-list">
|
||||
${this.devices.map(
|
||||
(id, _index, _ids) =>
|
||||
html`<vm-device-card .deviceID=${id}></vm-device-card>`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export { VMDeviceUI };
|
||||
|
||||
@@ -51,25 +51,49 @@ class VirtualMachine extends EventTarget {
|
||||
return this._devices;
|
||||
}
|
||||
|
||||
get deviceIds() {
|
||||
return Array.from(this.ic10vm.devices);
|
||||
}
|
||||
|
||||
get ics() {
|
||||
return this._ics;
|
||||
}
|
||||
|
||||
get icIds() {
|
||||
return Array.from(this.ic10vm.ics);
|
||||
}
|
||||
|
||||
get networks() {
|
||||
return Array.from(this.ic10vm.networks);
|
||||
}
|
||||
|
||||
get defaultNetwork() {
|
||||
return this.ic10vm.defaultNetwork;
|
||||
}
|
||||
|
||||
get activeIC() {
|
||||
return this._ics.get(window.App!.session.activeIC);
|
||||
}
|
||||
|
||||
visibleDevices(source: number) {
|
||||
const ids = Array.from(this.ic10vm.visibleDevices(source));
|
||||
return ids.map((id, _index) => this._devices.get(id)!);
|
||||
}
|
||||
|
||||
updateDevices() {
|
||||
var update_flag = false;
|
||||
const device_ids = this.ic10vm.devices;
|
||||
for (const id of device_ids) {
|
||||
if (!this._devices.has(id)) {
|
||||
this._devices.set(id, this.ic10vm.getDevice(id)!);
|
||||
update_flag = true;
|
||||
}
|
||||
}
|
||||
for (const id of this._devices.keys()) {
|
||||
if (!device_ids.includes(id)) {
|
||||
this._devices.get(id)!.free();
|
||||
this._devices.delete(id);
|
||||
update_flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,14 +101,21 @@ class VirtualMachine extends EventTarget {
|
||||
for (const id of ics) {
|
||||
if (!this._ics.has(id)) {
|
||||
this._ics.set(id, this._devices.get(id)!);
|
||||
update_flag = true;
|
||||
}
|
||||
}
|
||||
for (const id of this._ics.keys()) {
|
||||
if (!ics.includes(id)) {
|
||||
this._ics.get(id)!.free();
|
||||
this._ics.delete(id);
|
||||
update_flag = true;
|
||||
}
|
||||
}
|
||||
if (update_flag) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("vm-devices-update", { detail: device_ids }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateCode() {
|
||||
@@ -190,6 +221,7 @@ class VirtualMachine extends EventTarget {
|
||||
this.db = db;
|
||||
console.log("Loaded Device Database", this.db);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class VirtualMachineUI {
|
||||
|
||||
@@ -57,7 +57,7 @@ export class VMICRegisters extends VMActiveIC {
|
||||
}
|
||||
};
|
||||
const validation =
|
||||
"[-+]?(([0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?)|((\\.[0-9]+)([eE][+-]?[0-9]+)?)|([iI][nN][fF][iI][nN][iI][tT][yY]))";
|
||||
"[\\-+]?(([0-9]+(\\.[0-9]+)?([eE][\\-+]?[0-9]+)?)|((\\.[0-9]+)([eE][\\-+]?[0-9]+)?)|([iI][nN][fF][iI][nN][iI][tT][yY]))";
|
||||
const registerAliases: [string, number][] = (
|
||||
(
|
||||
[...(this.aliases ?? [])].filter(
|
||||
@@ -73,10 +73,10 @@ export class VMICRegisters extends VMActiveIC {
|
||||
<sl-card class="card">
|
||||
<div class="card-body">
|
||||
${this.registers?.map((val, index) => {
|
||||
const aliases = registerAliases
|
||||
.filter(([_alias, target]) => index === target)
|
||||
.map(([alias, _target]) => alias);
|
||||
return html`
|
||||
const aliases = registerAliases
|
||||
.filter(([_alias, target]) => index === target)
|
||||
.map(([alias, _target]) => alias);
|
||||
return html`
|
||||
<sl-tooltip placement="left" class="tooltip">
|
||||
<div slot="content">
|
||||
<strong>Regster r${index}</strong> Aliases:
|
||||
@@ -96,7 +96,7 @@ export class VMICRegisters extends VMActiveIC {
|
||||
</sl-input>
|
||||
</sl-tooltip>
|
||||
`;
|
||||
})}
|
||||
})}
|
||||
</div>
|
||||
</sl-card>
|
||||
`;
|
||||
|
||||
@@ -7,7 +7,6 @@ import "@shoelace-style/shoelace/dist/components/card/card.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/input/input.js";
|
||||
import { RegisterSpec } from "ic10emu_wasm";
|
||||
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
|
||||
|
||||
@customElement("vm-ic-stack")
|
||||
@@ -54,7 +53,7 @@ export class VMICStack extends VMActiveIC {
|
||||
}
|
||||
};
|
||||
const validation =
|
||||
"[-+]?(([0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?)|((\\.[0-9]+)([eE][+-]?[0-9]+)?)|([iI][nN][fF][iI][nN][iI][tT][yY]))";
|
||||
"[\\-+]?(([0-9]+(\\.[0-9]+)?([eE][\\-+]?[0-9]+)?)|((\\.[0-9]+)([eE][\\-+]?[0-9]+)?)|([iI][nN][fF][iI][nN][iI][tT][yY]))";
|
||||
const sp = this.registers![16];
|
||||
|
||||
return html`
|
||||
|
||||
@@ -2,19 +2,25 @@ import { HTMLTemplateResult, html, css } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
import { BaseElement, defaultCss } from "../components";
|
||||
import "@shoelace-style/shoelace/dist/components/details/details.js";
|
||||
import "@shoelace-style/shoelace/dist/components/tab/tab.js";
|
||||
import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js";
|
||||
import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js";
|
||||
|
||||
import "./controls";
|
||||
import "./registers";
|
||||
import "./stack";
|
||||
import "./device";
|
||||
|
||||
@customElement("vm-ui")
|
||||
export class VMUI extends BaseElement {
|
||||
static styles = [
|
||||
...defaultCss,
|
||||
css`
|
||||
sl-details {
|
||||
sl-tab-group {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
--indicator-color: var(--sl-color-purple-600);
|
||||
--sl-color-primary-600: var(--sl-color-purple-600);
|
||||
}
|
||||
sl-details::part(header) {
|
||||
padding: 0.3rem;
|
||||
@@ -42,12 +48,21 @@ export class VMUI extends BaseElement {
|
||||
return html`
|
||||
<div class="side-container">
|
||||
<vm-ic-controls></vm-ic-controls>
|
||||
<sl-details summary="Registers" open>
|
||||
<vm-ic-registers></vm-ic-registers>
|
||||
</sl-details>
|
||||
<sl-details summary="Stack">
|
||||
<vm-ic-stack></vm-ic-stack>
|
||||
</sl-details>
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel="active-ic">Active IC</sl-tab>
|
||||
<sl-tab slot="nav" panel="devices">Devices</sl-tab>
|
||||
<sl-tab-panel name="active-ic">
|
||||
<sl-details summary="Registers" open>
|
||||
<vm-ic-registers></vm-ic-registers>
|
||||
</sl-details>
|
||||
<sl-details summary="Stack">
|
||||
<vm-ic-stack></vm-ic-stack>
|
||||
</sl-details>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="devices">
|
||||
<vm-device-list></vm-device-list>
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user