allow device cards to collapse

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers
2024-04-10 02:16:12 -07:00
parent 36d72e9b73
commit 4f6d9df665
7 changed files with 327 additions and 194 deletions

View File

@@ -0,0 +1,94 @@
import { html, css, HTMLTemplateResult, PropertyValueMap } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import SlDetails from "@shoelace-style/shoelace/dist/components/details/details.js";
@customElement("ic10-details")
export class IC10Details extends SlDetails {
@query(".details__summary-icon") accessor summaryIcon: HTMLSpanElement;
constructor() {
super();
}
private handleSummaryIconClick(event: MouseEvent) {
event.preventDefault();
if (!this.disabled) {
if (this.open) {
this.hide();
} else {
this.show();
}
this.header.focus();
}
}
private handleSummaryIconKeyDown(event: KeyboardEvent) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
if (this.open) {
this.hide();
} else {
this.show();
}
}
if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
event.preventDefault();
this.hide();
}
if (event.key === "ArrowDown" || event.key === "ArrowRight") {
event.preventDefault();
this.show();
}
}
render() {
return html`
<details
part="base"
class=${classMap({
details: true,
"details--open": this.open,
"details--disabled": this.disabled,
})}
>
<summary
part="header"
id="header"
class="details__header"
role="button"
aria-expanded=${this.open ? "true" : "false"}
aria-controls="content"
aria-disabled=${this.disabled ? "true" : "false"}
tabindex=${this.disabled ? "-1" : "0"}
>
<slot name="summary" part="summary" class="details__summary"
>${this.summary}</slot
>
<span
part="summary-icon"
class="details__summary-icon"
@click=${this.handleSummaryIconClick}
@keydown=${this.handleSummaryIconKeyDown}
>
<slot name="expand-icon">
<sl-icon library="system" name="chevron-right"></sl-icon>
</slot>
<slot name="collapse-icon">
<sl-icon library="system" name="chevron-right"></sl-icon>
</slot>
</span>
</summary>
<div class="details__body" role="region" aria-labelledby="header">
<slot part="content" id="content" class="details__content"></slot>
</div>
</details>
`;
}
}

View File

@@ -1,3 +1,3 @@
import { BaseElement, defaultCss } from './base';
export { BaseElement, defaultCss }
import { IC10Details } from './details';
export { BaseElement, defaultCss, IC10Details }

View File

@@ -1,5 +1,5 @@
import { property, state } from "lit/decorators.js";
import { BaseElement } from "../components";
import {
DeviceRef,
Fields,
@@ -14,151 +14,179 @@ import {
Pins,
} from "ic10emu_wasm";
import { structuralEqual } from "../utils";
import { LitElement } from "lit";
import { BaseElement } from "../components/base";
export class VMBaseDevice extends BaseElement {
@property({ type: Number }) accessor deviceID: number;
@state() protected accessor device: DeviceRef;
type Constructor<T = {}> = new (...args: any[]) => T;
@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[];
@state() accessor icIP: number;
@state() accessor icOpCount: number;
@state() accessor icState: string;
@state() accessor errors: ICError[];
@state() accessor registers: Registers | null;
@state() accessor stack: Stack | null;
@state() accessor aliases: Aliases | null;
@state() accessor defines: Defines | null;
@state() accessor pins: Pins | null;
export declare class VMDeviceMixinInterface {
deviceID: number;
device: DeviceRef;
name: string | null;
nameHash: number | null;
prefabName: string | null;
fields: Fields;
slots: Slot[];
reagents: Reagents;
connections: Connection[];
icIP: number;
icOpCount: number;
icState: string;
errors: ICError[];
registers: Registers | null;
stack: Stack | null;
aliases: Aliases | null;
defines: Defines | null;
pins: Pins | null;
_handleDeviceModified(e: CustomEvent): void;
updateDevice(): void;
updateIC(): void;
}
constructor() {
super();
this.name = null;
this.nameHash = null;
export const VMDeviceMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMDeviceMixinClass extends superClass {
@property({ type: Number }) accessor deviceID: number;
@state() accessor device: DeviceRef;
@state() accessor name: string | null = null;
@state() accessor nameHash: number | null = null;
@state() accessor prefabName: string | null;
@state() accessor fields: Fields;
@state() accessor slots: Slot[];
@state() accessor reagents: Reagents;
@state() accessor connections: Connection[];
@state() accessor icIP: number;
@state() accessor icOpCount: number;
@state() accessor icState: string;
@state() accessor errors: ICError[];
@state() accessor registers: Registers | null;
@state() accessor stack: Stack | null;
@state() accessor aliases: Aliases | null;
@state() accessor defines: Defines | null;
@state() accessor pins: Pins | 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;
}
this.updateIC();
}
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 ?? null;
if (!structuralEqual(this.errors, errors)) {
this.errors = errors;
}
const registers = this.device.registers ?? null;
if (!structuralEqual(this.registers, registers)) {
this.registers = registers;
}
const stack = this.device.stack ?? null;
if (!structuralEqual(this.stack, stack)) {
this.stack = stack;
}
const aliases = this.device.aliases ?? null;
if (!structuralEqual(this.aliases, aliases)) {
this.aliases = aliases;
}
const defines = this.device.defines ?? null;
if (!structuralEqual(this.defines, defines)) {
this.defines = defines;
}
const pins = this.device.pins ?? null;
if (!structuralEqual(this.pins, pins)) {
this.pins = pins;
}
}
}
return VMDeviceMixinClass as Constructor<VMDeviceMixinInterface> & T;
};
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;
}
export const VMActiveICMixin = <T extends Constructor<LitElement>>(superClass: T) => {
class VMActiveICMixinClass extends VMDeviceMixin(superClass) {
constructor() {
super();
this.deviceID = window.App!.session.activeIC;
}
_handleDeviceModified(e: CustomEvent) {
const id = e.detail;
if (this.deviceID === id) {
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),
);
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();
}
}
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;
}
this.updateIC();
}
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 ?? null;
if (!structuralEqual(this.errors, errors)) {
this.errors = errors;
}
const registers = this.device.registers ?? null;
if (!structuralEqual(this.registers, registers)) {
this.registers = registers;
}
const stack = this.device.stack ?? null;
if (!structuralEqual(this.stack, stack)) {
this.stack = stack;
}
const aliases = this.device.aliases ?? null;
if (!structuralEqual(this.aliases, aliases)) {
this.aliases = aliases;
}
const defines = this.device.defines ?? null;
if (!structuralEqual(this.defines, defines)) {
this.defines = defines;
}
const pins = this.device.pins ?? null;
if(!structuralEqual(this.pins, pins)) {
this.pins = pins;
}
}
}
export class VMActiveIC extends VMBaseDevice {
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),
);
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();
}
};
return VMActiveICMixinClass as Constructor<VMDeviceMixinInterface> & T;
}

View File

@@ -1,7 +1,7 @@
import { html, css } from "lit";
import { customElement } from "lit/decorators.js";
import { defaultCss } from "../components";
import { VMActiveIC } from "./base_device";
import { BaseElement, defaultCss } from "../components";
import { VMActiveICMixin } from "./base_device";
import { structuralEqual } from "../utils";
import "@shoelace-style/shoelace/dist/components/card/card.js";
@@ -16,7 +16,7 @@ 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 {
export class VMICControls extends VMActiveICMixin(BaseElement) {
static styles = [
...defaultCss,
css`
@@ -121,11 +121,11 @@ export class VMICControls extends VMActiveIC {
@sl-change=${this._handleChangeActiveIC}
>
${ics.map(
([id, device], _index) =>
html`<sl-option value=${id}>
([id, device], _index) =>
html`<sl-option value=${id}>
Device:${id} ${device.name ?? device.prefabName}
</sl-option>`,
)}
)}
</sl-select>
</div>
</div>
@@ -148,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>

View File

@@ -1,8 +1,8 @@
import { Slot } from "ic10emu_wasm";
import { html, css, HTMLTemplateResult } from "lit";
import { html, css, HTMLTemplateResult, PropertyValueMap } from "lit";
import { customElement, state } from "lit/decorators.js";
import { BaseElement, defaultCss } from "../components";
import { VMBaseDevice } from "./base_device";
import { BaseElement, defaultCss, IC10Details } from "../components";
import { VMDeviceMixin } from "./base_device";
import "@shoelace-style/shoelace/dist/components/card/card.js";
import "@shoelace-style/shoelace/dist/components/icon/icon.js";
@@ -19,9 +19,10 @@ 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";
import SlDetails from "@shoelace-style/shoelace/dist/components/details/details.js";
@customElement("vm-device-card")
export class VMDeviceCard extends VMBaseDevice {
export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
image_err: boolean;
static styles = [
@@ -51,14 +52,12 @@ export class VMDeviceCard extends VMBaseDevice {
align-items: center;
flex-wrap: wrap;
}
// .device-name {
// box-sizing: border-box;
// width: 8rem;
// }
// .device-name-hash {
// box-sizing: border-box;
// width: 5rem;
// }
.device-name::part(input) {
width: 10rem;
}
.device-name-hash::part(input) {
width: 7rem;
}
sl-divider {
--spacing: 0.25rem;
}
@@ -129,7 +128,7 @@ export class VMDeviceCard extends VMBaseDevice {
value="${this.nameHash}"
disabled
>
<span slot="prefix">Name Hash</span>
<span slot="prefix">Hash</span>
<sl-copy-button
slot="suffix"
from="vmDeviceCard${this.deviceID}NameHash.value"
@@ -195,6 +194,7 @@ export class VMDeviceCard extends VMBaseDevice {
</sl-card>
`;
}
renderSlots(): HTMLTemplateResult {
return html`
<div clas="slots">
@@ -202,9 +202,11 @@ export class VMDeviceCard extends VMBaseDevice {
</div>
`;
}
renderReagents(): HTMLTemplateResult {
return html``;
}
renderNetworks(): HTMLTemplateResult {
const vmNetworks = window.VM!.networks;
return html`
@@ -260,10 +262,11 @@ export class VMDeviceCard extends VMBaseDevice {
</div>
`;
}
protected render(): HTMLTemplateResult {
render(): HTMLTemplateResult {
return html`
<sl-card class="card">
<div class="header" slot="header">${this.renderHeader()}</div>
<ic10-details class="device-card" open>
<div class="header" slot="summary">${this.renderHeader()}</div>
<sl-tab-group>
<sl-tab slot="nav" panel="fields">Fields</sl-tab>
<sl-tab slot="nav" panel="slots">Slots</sl-tab>
@@ -277,7 +280,7 @@ export class VMDeviceCard extends VMBaseDevice {
<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>
</ic10-details>
`;
}
@@ -341,6 +344,19 @@ export class VMDeviceCard extends VMBaseDevice {
export class VMDeviceList extends BaseElement {
@state() accessor devices: number[];
static styles = [
...defaultCss,
css`
.device-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.device-list-card {
}
`,
];
constructor() {
super();
this.devices = window.VM!.deviceIds;
@@ -367,7 +383,10 @@ export class VMDeviceList extends BaseElement {
<div class="device-list">
${this.devices.map(
(id, _index, _ids) =>
html`<vm-device-card .deviceID=${id}></vm-device-card>`,
html`<vm-device-card
.deviceID=${id}
class="device-list-card"
></vm-device-card>`,
)}
</div>
`;

View File

@@ -1,7 +1,7 @@
import { html, css } from "lit";
import { customElement } from "lit/decorators.js";
import { defaultCss } from "../components";
import { VMActiveIC } from "./base_device";
import { BaseElement, defaultCss } from "../components";
import { VMActiveICMixin } from "./base_device";
import "@shoelace-style/shoelace/dist/components/card/card.js";
import "@shoelace-style/shoelace/dist/components/icon/icon.js";
@@ -12,7 +12,7 @@ import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
import { parseNumber } from "../utils";
@customElement("vm-ic-registers")
export class VMICRegisters extends VMActiveIC {
export class VMICRegisters extends VMActiveICMixin(BaseElement) {
static styles = [
...defaultCss,
css`
@@ -57,8 +57,6 @@ export class VMICRegisters extends VMActiveIC {
return val.toString();
}
};
const validation =
"[\\-+]?(([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(
@@ -74,19 +72,18 @@ 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:
<strong>Register r${index}</strong> Aliases:
<em>${aliases.join(", ") || "None"}</em>
</div>
<sl-input
type="text"
value="${displayVal(val)}"
pattern="${validation}"
size="small"
class="reg-input"
@sl-change=${this._handleCellChange}
@@ -97,7 +94,7 @@ export class VMICRegisters extends VMActiveIC {
</sl-input>
</sl-tooltip>
`;
})}
})}
</div>
</sl-card>
`;
@@ -106,7 +103,7 @@ export class VMICRegisters extends VMActiveIC {
_handleCellChange(e: Event) {
const input = e.target as SlInput;
const index = parseInt(input.getAttribute("key")!);
const val = parseNumber(input.value)
const val = parseNumber(input.value);
window.VM!.setRegister(index, val);
}
}

View File

@@ -1,7 +1,7 @@
import { html, css } from "lit";
import { customElement } from "lit/decorators.js";
import { defaultCss } from "../components";
import { VMActiveIC } from "./base_device";
import { BaseElement, defaultCss } from "../components";
import { VMActiveICMixin } from "./base_device";
import "@shoelace-style/shoelace/dist/components/card/card.js";
import "@shoelace-style/shoelace/dist/components/icon/icon.js";
@@ -11,7 +11,7 @@ import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
import { parseNumber } from "../utils";
@customElement("vm-ic-stack")
export class VMICStack extends VMActiveIC {
export class VMICStack extends VMActiveICMixin(BaseElement) {
static styles = [
...defaultCss,
css`
@@ -53,8 +53,6 @@ export class VMICStack extends VMActiveIC {
return val.toString();
}
};
const validation =
"[\\-+]?(([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`
@@ -62,17 +60,14 @@ export class VMICStack extends VMActiveIC {
<div class="card-body">
${this.stack?.map((val, index) => {
return html`
<sl-tooltip
placement="left"
>
<sl-tooltip placement="left">
<div slot="content">
${sp === index ? html`<strong>Stack Pointer</strong>` : ""}
Address ${index}
${sp === index ? html`<strong>Stack Pointer</strong>` : ""}
Address ${index}
</div>
<sl-input
type="text"
value="${displayVal(val)}"
pattern="${validation}"
size="small"
class="stack-input ${sp === index ? "stack-pointer" : ""}"
@sl-change=${this._handleCellChange}
@@ -91,7 +86,7 @@ export class VMICStack extends VMActiveIC {
_handleCellChange(e: Event) {
const input = e.target as SlInput;
const index = parseInt(input.getAttribute("key")!);
const val = parseNumber(input.value)
const val = parseNumber(input.value);
window.VM!.setStack(index, val);
}
}