mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-28 08:58:07 +00:00
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:
@@ -0,0 +1,37 @@
|
||||
import { LitElement } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
export abstract class FormFieldBaseElement extends LitElement {
|
||||
static formAssociated = true;
|
||||
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@property({ type: Boolean }) readonly = false;
|
||||
@property({ type: Number }) override tabIndex = 0;
|
||||
|
||||
get form() { return this.internals.form; }
|
||||
get name() { return this.getAttribute("name"); }
|
||||
get type() { return this.localName; }
|
||||
|
||||
get validity() { return this.internals.validity; }
|
||||
get validationMessage() { return this.internals.validationMessage; }
|
||||
get willValidate() { return this.internals.willValidate; }
|
||||
|
||||
protected internals: ElementInternals;
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.internals = this.attachInternals();
|
||||
}
|
||||
|
||||
checkValidity(): boolean {
|
||||
return this.internals.checkValidity();
|
||||
}
|
||||
|
||||
reportValidity(): boolean {
|
||||
return this.internals.reportValidity();
|
||||
}
|
||||
|
||||
formDisabledCallback(disabled: boolean): void {
|
||||
this.disabled = disabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./text-field.element";
|
||||
@@ -0,0 +1,40 @@
|
||||
@use "sass:math";
|
||||
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
background: #0004;
|
||||
border: 1px solid #FFF2;
|
||||
color: #FFFC;
|
||||
transition: {
|
||||
property: background-color, border-color, color;
|
||||
duration: 33.333ms;
|
||||
timing-function: linear;
|
||||
}
|
||||
|
||||
font: {
|
||||
family: "Fira Sans", sans-serif;
|
||||
size: math.div(15, 16) * 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
:host(:focus) {
|
||||
outline: none;
|
||||
border-color: #FFF4;
|
||||
background: #0006;
|
||||
color: #FFFF;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
outline: none !important;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { on } from "@storyteller/framework";
|
||||
import { html, unsafeCSS } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { FormFieldBaseElement } from "./form-field-base.element";
|
||||
|
||||
import styles from "./text-field.element.scss?inline";
|
||||
|
||||
@customElement("sts-text-field")
|
||||
export class TextFieldElement extends FormFieldBaseElement {
|
||||
static override styles = unsafeCSS(styles);
|
||||
static override shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
}
|
||||
|
||||
@property() value = "";
|
||||
|
||||
@on("keydown")
|
||||
@on("keyup")
|
||||
blockKeypressBubbling(event: KeyboardEvent): void {
|
||||
if (event.key !== "Alt" && event.key !== "Control")
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
#onInput(event: InputEvent): void {
|
||||
const updated = (event.target as HTMLInputElement).value;
|
||||
console.log("updated:", updated);
|
||||
|
||||
this.dispatchEvent(new CustomEvent("value-change", { detail: updated }));
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
.value=${this.value}
|
||||
@input=${this.#onInput}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
@@ -28,6 +28,9 @@ import fasCheck from "@fortawesome/fontawesome-pro/svgs/solid/check.svg?raw";
|
||||
import fasXmark from "@fortawesome/fontawesome-pro/svgs/solid/xmark.svg?raw";
|
||||
import fasCircleCheck from "@fortawesome/fontawesome-pro/svgs/solid/circle-check.svg?raw";
|
||||
import fasCircleXmark from "@fortawesome/fontawesome-pro/svgs/solid/circle-xmark.svg?raw";
|
||||
import fasMinus from "@fortawesome/fontawesome-pro/svgs/solid/minus.svg?raw";
|
||||
import fasListTree from "@fortawesome/fontawesome-pro/svgs/solid/list-tree.svg?raw";
|
||||
import fasBars from "@fortawesome/fontawesome-pro/svgs/solid/bars.svg?raw";
|
||||
|
||||
import styles from "./icon.element.scss?inline";
|
||||
|
||||
@@ -54,6 +57,7 @@ function renderSvg(iconId: string) {
|
||||
return RENDER_RESULTS.get(iconId)!;
|
||||
|
||||
const svgSrc = match (iconId, {
|
||||
"bars": () => fasBars,
|
||||
"bone": () => farBone,
|
||||
"bwd-fast": () => fasBwdFast,
|
||||
"bwd-step": () => fasBwdStep,
|
||||
@@ -69,6 +73,8 @@ function renderSvg(iconId: string) {
|
||||
"fwd-step": () => fasFwdStep,
|
||||
"key": () => fasKey,
|
||||
"light": () => fasBrightness,
|
||||
"list-tree": () => fasListTree,
|
||||
"minus": () => fasMinus,
|
||||
"globe": () => farGlobe,
|
||||
"play": () => fasPlay,
|
||||
"question": () => fasQuestion,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { on } from "@storyteller/framework";
|
||||
import * as studio from "@storyteller/studio";
|
||||
import { type EntitySelectEvent } from "@storyteller/studio";
|
||||
import { LitElement, html, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
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";
|
||||
|
||||
@@ -10,6 +8,12 @@ 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;
|
||||
@@ -26,20 +30,40 @@ export class TreeElement extends LitElement {
|
||||
@property({ attribute: false }) treeChildren?: Tree[];
|
||||
@property({ type: Boolean }) expanded = true;
|
||||
|
||||
@state() selected = false;
|
||||
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;
|
||||
}
|
||||
|
||||
@on("window:entity-select")
|
||||
onEntitySelect({ detail: entity }: EntitySelectEvent): void {
|
||||
this.selected = this.id === entity;
|
||||
}
|
||||
|
||||
selectEntity(): void {
|
||||
if (this.id)
|
||||
studio.select(this.id);
|
||||
if (this.id) {
|
||||
this.dispatchEvent(new CustomEvent("tree-select", {
|
||||
detail: this.id,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: A11y
|
||||
|
||||
Reference in New Issue
Block a user