Files
storyteller-bevy/studio-ui/src/lib/tree.element.ts
T
Danny McGee c9904a5293 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
2024-03-04 15:13:03 -05:00

115 lines
2.8 KiB
TypeScript

import { type DynamicProvider, inject, UniqueToken } from "@storyteller/framework";
import { LitElement, type PropertyValues, html, nothing, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
import "./icon.element";
import styles from "./tree.element.scss?inline";
export const TREE_SELECTION_PROVIDER = UniqueToken.create<TreeSelectionProvider>();
export interface TreeSelectionProvider extends DynamicProvider<TreeSelectionProvider> {
readonly selectedId: string | null;
}
export interface Tree {
id?: string;
name: string;
icon?: string;
children?: Tree[];
}
@customElement("sts-tree")
export class TreeElement extends LitElement {
static override styles = unsafeCSS(styles);
@property() name = "";
@property() icon?: string;
@property({ attribute: false }) treeChildren?: Tree[];
@property({ type: Boolean }) expanded = true;
get selected() {
return !!this.id && this._selectionProvider?.selectedId === this.id
}
@inject(TREE_SELECTION_PROVIDER)
_selectionProvider?: TreeSelectionProvider;
override connectedCallback(): void {
this._selectionProvider?.addEventListener("property-changes", this.onProviderChanges);
super.connectedCallback();
}
override disconnectedCallback(): void {
this._selectionProvider?.removeEventListener("property-changes", this.onProviderChanges);
super.disconnectedCallback();
}
onProviderChanges = ({ detail: changes }: CustomEvent<PropertyValues<TreeSelectionProvider>>) => {
if (changes.has("selectedId"))
this.requestUpdate();
}
toggleExpanded(): void {
this.expanded = !this.expanded;
}
selectEntity(): void {
if (this.id) {
this.dispatchEvent(new CustomEvent("tree-select", {
detail: this.id,
bubbles: true,
composed: true,
}));
}
}
// TODO: A11y
protected override render = () => html`
<header
class=${classMap({
selected: this.selected,
selectable: !!this.id,
})}
>
${this.treeChildren?.length ? html`
<button class="toggle-expanded"
@click=${this.toggleExpanded}
>
<sts-icon
class=${classMap({
caret: true,
expanded: this.expanded,
})}
icon="caret-right"
aria-label=${this.expanded ? "Expand" : "Collapse"}
></sts-icon>
</button>
` : nothing}
<span class="label"
@click=${this.selectEntity}
>
${this.icon ? html`
<sts-icon class="icon" icon=${this.icon}></sts-icon>
` : nothing}
${this.name}
</span>
</header>
${this.expanded && this.treeChildren?.length ? html`
<div class="children">
${this.treeChildren?.map(tree => html`
<sts-tree
id=${ifDefined(tree.id)}
.name=${tree.name}
.icon=${tree.icon}
.treeChildren=${tree.children}
></sts-tree>
`)}
</div>
` : nothing}
`;
}