Inspector frontend UI (#68)

This PR begins fleshing out the front-end UI for inspecting the selected
entity's components.

### Implemented
* Support for the `Name` component
* Placeholder/WIP support for the `Transform` component

### To-do
* Support for sending changes made in the inspector back to Bevy
* A slider/field combo widget for numeric values (can be adapted from
  [this StackBlitz](https://stackblitz.com/edit/angular-wfikuy?file=src%2Ffloat-input%2Ffloat-input.component.ts))
* Quat -> Euler angles conversion ([example math here](https://github.com/dannymcgee/3DGameMath/blob/5cb7a8ff9310565329e57751cd8b9c33e9bd4cb7/libs/Math/include/math/quat.inl.hpp#L56-L78),
  or use a 3rd-party math library)

### Known issues
* Selecting entities from the hierarchy doesn't always update the
  components list. I suspect this is the same issue as #39
This commit is contained in:
Danny McGee
2024-03-04 15:13:03 -05:00
committed by GitHub
parent 9875ea7cca
commit c9904a5293
26 changed files with 1093 additions and 174 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
export * from "./lib/decorators";
export * from "./lib/di";
export { default as spread } from "./lib/directives/spread";
export { default as bind, type Props as BindProps } from "./lib/directives/bind";
export { default as drag, type Props as DragProps } from "./lib/directives/drag";
export { default as spread } from "./lib/directives/spread";
+19 -2
View File
@@ -1,13 +1,18 @@
import { AbstractCtor, Ctor } from "@storyteller/utility";
import { type AbstractCtor, type Ctor, type Fn } from "@storyteller/utility";
import { type PropertyValues } from "lit";
export class UniqueToken {
get id() { return this.#id; }
readonly #id: symbol;
constructor (name: string) {
private constructor (name: string) {
this.#id = Symbol(name);
}
static create<T>(name?: string): Token<T> {
return new UniqueToken(name ?? crypto.randomUUID());
}
toString(): string {
return `UniqueToken(${String(this.#id)})`;
}
@@ -23,3 +28,15 @@ export interface Provider<T> {
token: Token<T>;
value: T;
}
export interface DynamicProvider<T> {
addEventListener(
type: "property-changes",
listener: Fn<[CustomEvent<PropertyValues<T>>], any>,
): void;
removeEventListener(
type: "property-changes",
listener: Fn<[CustomEvent<PropertyValues<T>>], any>,
): void;
}
+146
View File
@@ -0,0 +1,146 @@
import { match } from "@storyteller/utility";
import { AttributePart, LitElement } from "lit";
import {
Directive,
DirectiveParameters,
PartInfo,
PartType,
directive,
} from "lit/directive.js";
export type Props<T extends LitElement, K extends keyof T> = [T, K];
class BindDirective<T extends LitElement, K extends keyof T> extends Directive {
#host?: HTMLElement;
#hostKey: string;
#listener?: (this: T, event: CustomEvent<T[K]>) => void;
constructor (info: PartInfo) {
super(info);
if (
info.type !== PartType.ATTRIBUTE
&& info.type !== PartType.BOOLEAN_ATTRIBUTE
&& info.type !== PartType.PROPERTY
) {
throw new Error(`The 'bind' directive must be used on an attribute`);
}
this.#hostKey = info.name;
}
override render([receiver, key]: Props<T, K>) {
return receiver[key];
}
override update(part: AttributePart, [[receiver, key]]: DirectiveParameters<this>) {
this.#host ??= part.element;
if (!this.#listener)
this.#addListener(part.element, receiver, key);
return this.render([receiver, key]);
}
#addListener(host: HTMLElement, receiver: T, key: K): void {
const proto = receiver.constructor.prototype;
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
if (!descriptor || !descriptor.set) {
const className = receiver.constructor.name;
const printableKey = match (typeof key, {
"string": () => `"${key as string}"`,
"number": () => `${key as number}`,
"symbol": () => `Symbol(${String(key)})`,
_: () => {
throw new Error("Unreachable");
}
});
throw new Error(
`'bind' directive expected ${className}[${printableKey}] to be a \`@state()\` property`,
);
}
this.#listener = function (this: T, event: CustomEvent<T[K]>) {
descriptor.set!.call(this, event.detail);
}
host.addEventListener(`${this.#hostKey}-change`, this.#listener.bind(receiver) as any);
}
}
/**
* Configures two-way binding for a property.
*
* @example
* ```
* \@customElement("sts-receiver")
* export class ReceiverElement extends LitElement {
* \@state() receiverValue = "";
*
* render = () => html`
* <sts-host .hostValue=${bind(this, "receiverValue")}></sts-host>
* `;
* }
*
* \@customElement("sts-host")
* export class HostElement extends LitElement {
* \@property() hostValue = "";
*
* emitNewValue(event: InputEvent) {
* this.dispatchEvent(new CustomEvent("hostValue-change", {
* detail: event.target.value
* }));
* }
*
* render = () => html`
* <input
* type="text"
* .value=${this.hostValue}
* \@input=${this.emitNewValue}
* />
* `;
* }
* ```
*
* @note
* Two-way binding works as follows:
*
* - The property bound in the "receiver" element (`ReceiverElement` in the
* example above) must be configured with `get` and `set` accessors. This is
* done automatically when using the `@state()` decorator to annotate the
* property.
*
* - The "host" element (`HostElement` in the example above) should emit a
* `CustomEvent` named "&lt;property&gt;-change" whenever the property value
* should change. (The new value should be emitted as the event `detail`.)
*
* - "&lt;property&gt;-change" events emitting from the host element will
* propagate up to the receiver, and changes to the value of the receiver
* property will propagate down to the host element as normal.
*
* A simpler way to explain all this is to say that the following example is
* functionally equivalent to the example above:
*
* ```ts
* \@customElement("sts-receiver")
* export class ReceiverElement extends LitElement {
* \@state() receiverValue = "";
*
* onHostValueChange(event: CustomEvent<string>) {
* this.receiverValue = event.detail;
* }
*
* render = () => html`
* <sts-host
* .hostValue=${this.receiverValue)}
* \@hostValue-change=${this.onHostValueChange}
* ></sts-host>
* `;
* }
* ```
*/
export default function <T extends LitElement, K extends keyof T>(receiver: T, key: K) {
return directive(BindDirective<T, K>)([receiver, key]);
}