You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
764 lines
28 KiB
764 lines
28 KiB
1 year ago
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Linq;
|
||
|
using System.Numerics;
|
||
|
using gaemstone.Client.Utility;
|
||
|
using gaemstone.ECS;
|
||
|
using gaemstone.Flecs;
|
||
|
using gaemstone.Utility;
|
||
|
using ImGuiNET;
|
||
|
using static gaemstone.Client.Systems.ImGuiManager;
|
||
|
using Icon = gaemstone.Client.Utility.ForkAwesome;
|
||
|
using ImGuiInternal = ImGuiNET.Internal.ImGui;
|
||
|
|
||
|
namespace gaemstone.Client.Systems;
|
||
|
|
||
|
[Module]
|
||
|
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
|
||
|
public class EntityInspector
|
||
|
: IModuleInitializer
|
||
|
{
|
||
|
[Tag]
|
||
|
public struct InspectorWindow { }
|
||
|
|
||
|
[Relation, Exclusive]
|
||
|
[Add<DeletionEvent.OnDeleteTarget, DeletionBehavior.Delete>]
|
||
|
public struct Selected { }
|
||
|
|
||
|
[Tag]
|
||
|
public struct ScrollToSelected { }
|
||
|
|
||
|
[Relation]
|
||
|
public struct Expanded { }
|
||
|
|
||
|
[Component]
|
||
|
public class History
|
||
|
{
|
||
|
public Entry? Current { get; set; } = null;
|
||
|
|
||
|
public class Entry
|
||
|
{
|
||
|
public Entity Entity { get; }
|
||
|
public EntityPath? Path { get; }
|
||
|
|
||
|
public Entry? Prev { get; set; }
|
||
|
public Entry? Next { get; set; }
|
||
|
|
||
|
public Entry(EntityRef entity, Entry? prev, Entry? next)
|
||
|
{
|
||
|
Entity = entity;
|
||
|
Path = entity.GetFullPath();
|
||
|
if ((Prev = prev) != null) Prev.Next = this;
|
||
|
if ((Next = next) != null) Next.Prev = this;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
[Component]
|
||
|
public struct DocPriority { public float Value; }
|
||
|
|
||
|
[Component]
|
||
|
public struct DocIcon { public char Value; }
|
||
|
|
||
|
|
||
|
public void Initialize(EntityRef module)
|
||
|
{
|
||
|
void SetDocInfo(string path, float priority, string icon, float r, float g, float b)
|
||
|
=> module.World.LookupByPathOrThrow(path)
|
||
|
.Add<Doc.DisplayType>()
|
||
|
.Set(new DocPriority { Value = priority })
|
||
|
.Set(new DocIcon { Value = icon[0] })
|
||
|
.SetDocColor(Color.FromRGB(r, g, b).ToHexString());
|
||
|
|
||
|
SetDocInfo("/flecs/core/Module" , 0 , Icon.Archive , 1.0f, 0.9f, 0.7f);
|
||
|
SetDocInfo("/flecs/system/System" , 1 , Icon.Cog , 1.0f, 0.7f, 0.7f);
|
||
|
SetDocInfo("/flecs/core/Observer" , 2 , Icon.Eye , 1.0f, 0.8f, 0.8f);
|
||
|
SetDocInfo("/gaemstone/Doc/Relation" , 3 , Icon.ShareAlt , 0.7f, 1.0f, 0.8f);
|
||
|
SetDocInfo("/flecs/core/Tag" , 4 , Icon.Tag , 0.7f, 0.8f, 1.0f);
|
||
|
SetDocInfo("/flecs/core/Component" , 5 , Icon.PencilSquare , 0.6f, 0.6f, 1.0f);
|
||
|
SetDocInfo("/flecs/core/Prefab" , 6 , Icon.Cube , 0.9f, 0.8f, 1.0f);
|
||
|
}
|
||
|
|
||
|
|
||
|
[System]
|
||
|
public void ShowUIButton(World world, ImGuiData _)
|
||
|
{
|
||
|
var hasAnyInspector = false;
|
||
|
var inspectorWindow = world.LookupByTypeOrThrow<InspectorWindow>();
|
||
|
foreach (var entity in Iterator.FromTerm(world, new(inspectorWindow)))
|
||
|
{ hasAnyInspector = true; break; }
|
||
|
|
||
|
if (ImGuiUtility.UIButton(0, Icon.Search, "Entity Inspector", hasAnyInspector))
|
||
|
NewEntityInspectorWindow(world);
|
||
|
}
|
||
|
|
||
|
[System]
|
||
|
public void ShowExplorerWindow(EntityRef window, InspectorWindow _, History? history)
|
||
|
{
|
||
|
var isOpen = true;
|
||
|
var fontSize = ImGui.GetFontSize();
|
||
|
var viewCenter = ImGui.GetMainViewport().GetCenter();
|
||
|
ImGui.SetNextWindowPos(viewCenter, ImGuiCond.Appearing, new(0.5f, 0.5f));
|
||
|
ImGui.SetNextWindowSize(new(fontSize * 40, fontSize * 25), ImGuiCond.Appearing);
|
||
|
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]);
|
||
|
if (ImGui.Begin($"{Icon.Search} Entity Inspector##{window.Id}",
|
||
|
ref isOpen, ImGuiWindowFlags.NoScrollbar)) {
|
||
|
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[0]);
|
||
|
|
||
|
var selected = window.GetTargets<Selected>().FirstOrDefault();
|
||
|
ActionBarAndPath(window, history, selected);
|
||
|
|
||
|
ImGui.BeginTable("Views", 2, ImGuiTableFlags.Resizable);
|
||
|
ImGui.TableSetupColumn("Explorer", ImGuiTableColumnFlags.WidthFixed, fontSize * 12);
|
||
|
ImGui.TableSetupColumn("Entity", ImGuiTableColumnFlags.WidthStretch);
|
||
|
|
||
|
ImGui.TableNextColumn();
|
||
|
ImGui.BeginChild("ExplorerView", new(-float.Epsilon, -float.Epsilon));
|
||
|
ExplorerView(window, history, selected);
|
||
|
ImGui.EndChild();
|
||
|
|
||
|
void Tab(string name, Action<EntityRef, History?, EntityRef?> contentMethod)
|
||
|
{
|
||
|
if (!ImGui.BeginTabItem(name)) return;
|
||
|
ImGui.BeginChild($"{name}Tab", new(-float.Epsilon, -float.Epsilon));
|
||
|
contentMethod(window, history, selected);
|
||
|
ImGui.EndChild();
|
||
|
ImGui.EndTabItem();
|
||
|
}
|
||
|
|
||
|
ImGui.TableNextColumn();
|
||
|
ImGui.BeginChild("EntityView", new(-float.Epsilon, -float.Epsilon));
|
||
|
if (!ImGui.BeginTabBar("Tabs")) return;
|
||
|
Tab($"{Icon.PencilSquare} Components", ComponentsTab);
|
||
|
Tab($"{Icon.ShareAlt} References", ReferencesTab);
|
||
|
Tab($"{Icon.InfoCircle} Documentation", DocumentationTab);
|
||
|
ImGui.EndTabBar();
|
||
|
ImGui.EndChild();
|
||
|
|
||
|
ImGui.EndTable();
|
||
|
|
||
|
ImGui.PopFont();
|
||
|
}
|
||
|
ImGui.PopFont();
|
||
|
ImGui.End();
|
||
|
|
||
|
// If window is closed, delete the entity.
|
||
|
if (!isOpen) window.Delete();
|
||
|
}
|
||
|
|
||
|
[Observer<ObserverEvent.OnRemove>]
|
||
|
public void ClearStorageOnRemove(EntityRef _1, InspectorWindow _2)
|
||
|
{
|
||
|
// TODO: Clear out settings store for the window.
|
||
|
}
|
||
|
|
||
|
private void ActionBarAndPath(EntityRef window, History? history, EntityRef? selected)
|
||
|
{
|
||
|
var world = window.World;
|
||
|
|
||
|
static bool IconButtonWithToolTip(string icon, string tooltip, bool enabled = true) {
|
||
|
if (!enabled) ImGui.BeginDisabled();
|
||
|
var clicked = ImGui.Button(icon);
|
||
|
if (!enabled) ImGui.EndDisabled();
|
||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||
|
ImGui.SetTooltip(tooltip);
|
||
|
return clicked;
|
||
|
}
|
||
|
|
||
|
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, ImGui.GetStyle().ItemSpacing.Y));
|
||
|
ImGui.BeginTable("ActionBar", 3);
|
||
|
ImGui.TableSetupColumn("Explorer", ImGuiTableColumnFlags.WidthFixed);
|
||
|
ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch);
|
||
|
ImGui.TableSetupColumn("Entity", ImGuiTableColumnFlags.WidthFixed);
|
||
|
|
||
|
ImGui.TableNextColumn();
|
||
|
var hasExpanded = window.Has<Expanded, Core.Wildcard>();
|
||
|
if (IconButtonWithToolTip(Icon.Outdent, "Collapse all items in the Explorer View", hasExpanded))
|
||
|
window.Remove<Expanded, Core.Wildcard>();
|
||
|
|
||
|
if (history != null) {
|
||
|
var hasPrev = ((selected != null) ? history.Current?.Prev : history.Current) != null;
|
||
|
var hasNext = history.Current?.Next != null;
|
||
|
ImGui.SameLine();
|
||
|
if (IconButtonWithToolTip(Icon.ArrowLeft, "Go to the previously viewed entity", hasPrev))
|
||
|
GoToPrevious(window, history, selected);
|
||
|
ImGui.SameLine();
|
||
|
if (IconButtonWithToolTip(Icon.ArrowRight, "Go to the next viewed entity", hasNext))
|
||
|
GoToNext(window, history);
|
||
|
}
|
||
|
|
||
|
ImGui.SameLine();
|
||
|
if (IconButtonWithToolTip(Icon.Crosshairs, "Scroll to the current entity in the Explorer View", (selected != null)))
|
||
|
window.Add<ScrollToSelected>();
|
||
|
|
||
|
ImGui.TableNextColumn();
|
||
|
var availableWidth = ImGui.GetColumnWidth() - ImGui.GetStyle().CellPadding.X * 2;
|
||
|
PathInput(window, history, selected, availableWidth);
|
||
|
|
||
|
ImGui.TableNextColumn();
|
||
|
if (IconButtonWithToolTip(Icon.PlusCircle, "Create a new child entity", (selected != null)))
|
||
|
// FIXME: Replace this once Flecs has been fixed.
|
||
|
SetSelected(window, history, world.New().Build().ChildOf(selected));
|
||
|
// SelectAndScrollTo(windowEntity, windowData, selected!.NewChild().Build(), selected);
|
||
|
|
||
|
ImGui.SameLine();
|
||
|
if (IconButtonWithToolTip(Icon.Pencil, "Rename the current entity", false && (selected != null)))
|
||
|
{ } // TODO: Implement this!
|
||
|
|
||
|
ImGui.SameLine();
|
||
|
var isDisabled = (selected?.IsDisabled == true);
|
||
|
var icon = !isDisabled ? Icon.BellSlash : Icon.Bell;
|
||
|
var tooltip = $"{(!isDisabled ? "Disable" : "Enable")} the current entity";
|
||
|
if (IconButtonWithToolTip(icon, tooltip, (selected != null)))
|
||
|
{ if (isDisabled) selected!.Enable(); else selected!.Disable(); }
|
||
|
|
||
|
ImGui.SameLine();
|
||
|
if (IconButtonWithToolTip(Icon.Trash, "Delete the current entity", (selected != null))) {
|
||
|
SetSelected(window, history, selected!.Parent);
|
||
|
selected.Delete(); // TODO: Confirmation dialog?
|
||
|
}
|
||
|
|
||
|
ImGui.EndTable();
|
||
|
ImGui.PopStyleVar();
|
||
|
}
|
||
|
|
||
|
private void PathInput(EntityRef window, History? history, EntityRef? selected, float availableWidth)
|
||
|
{
|
||
|
var style = ImGui.GetStyle();
|
||
|
ImGui.AlignTextToFramePadding();
|
||
|
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, style.ItemSpacing.Y));
|
||
|
|
||
|
var path = selected?.GetFullPath() ?? null;
|
||
|
|
||
|
if (path != null) {
|
||
|
var visiblePath = path.GetParts().ToList();
|
||
|
var visiblePathTextSize = ImGui.CalcTextSize(path.ToString()).X
|
||
|
+ style.ItemSpacing.X * 2 * (path.Count - 0.5f)
|
||
|
+ style.FramePadding.X * 2 * path.Count;
|
||
|
while ((visiblePath.Count > 3) && (visiblePathTextSize > availableWidth)) {
|
||
|
if (visiblePath[1] != "...") {
|
||
|
visiblePathTextSize -= ImGui.CalcTextSize(visiblePath[1]).X - ImGui.CalcTextSize("...").X;
|
||
|
visiblePath[1] = "...";
|
||
|
} else {
|
||
|
visiblePathTextSize -= ImGui.CalcTextSize(visiblePath[2] + "/").X
|
||
|
+ (style.ItemSpacing.X + style.FramePadding.X) * 2;
|
||
|
visiblePath.RemoveAt(2);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var numHiddenItems = path.Count - visiblePath.Count;
|
||
|
for (var i = 0; i < visiblePath.Count - 1; i++) {
|
||
|
var actualIndex = (i == 0) ? 0 : i + numHiddenItems;
|
||
|
ImGui.Text("/");
|
||
|
ImGui.SameLine();
|
||
|
if (visiblePath[i] == "...") {
|
||
|
ImGui.BeginDisabled();
|
||
|
ImGui.Button("...");
|
||
|
ImGui.EndDisabled();
|
||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||
|
ImGui.SetTooltip(path[1..(numHiddenItems+2)].ToString());
|
||
|
} else if (ImGui.Button(visiblePath[i]))
|
||
|
SetSelected(window, history, window.World.LookupByPath(path[..(actualIndex+1)]));
|
||
|
ImGui.SameLine();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ImGui.Text("/"); ImGui.SameLine();
|
||
|
var name = path?.Name.ToString() ?? "";
|
||
|
ImGui.SetNextItemWidth(-float.Epsilon);
|
||
|
ImGui.InputText("##Path", ref name, 256);
|
||
|
|
||
|
ImGui.PopStyleVar();
|
||
|
}
|
||
|
|
||
|
private struct EntitySummary
|
||
|
: IComparable<EntitySummary>
|
||
|
{
|
||
|
public Entity Entity { get; init; }
|
||
|
public SpecialType? Type { get; init; }
|
||
|
public string? Name { get; init; }
|
||
|
public string? DocName { get; init; }
|
||
|
public Color? DocColor { get; init; }
|
||
|
public bool HasChildren { get; init; }
|
||
|
public bool IsExpanded { get; init; }
|
||
|
public bool IsDisabled { get; init; }
|
||
|
|
||
|
public int CompareTo(EntitySummary other)
|
||
|
{
|
||
|
static int? Compare<T>(T x, T y) {
|
||
|
if (x is null) { if (y is null) return null; else return 1; }
|
||
|
else if (y is null) return -1;
|
||
|
var result = Comparer<T>.Default.Compare(x, y);
|
||
|
return (result != 0) ? result : null;
|
||
|
}
|
||
|
|
||
|
return Compare(Type, other.Type)
|
||
|
?? Compare(Name, other.Name)
|
||
|
?? Compare(DocName, other.DocName)
|
||
|
?? Compare(Entity.Id, other.Entity.Id)
|
||
|
?? 0;
|
||
|
}
|
||
|
|
||
|
public string DisplayName { get {
|
||
|
var name = (DocName != null) ? $"\"{DocName}\"" : Name ?? Entity.Id.ToString();
|
||
|
if (Type != null) name = $"{DisplayIcon} {name}";
|
||
|
return name;
|
||
|
} }
|
||
|
|
||
|
public string? DisplayIcon => Type switch {
|
||
|
SpecialType.Module => Icon.Archive,
|
||
|
SpecialType.System => Icon.Cog,
|
||
|
SpecialType.Relation => Icon.ShareAlt,
|
||
|
SpecialType.Component => Icon.PencilSquare,
|
||
|
SpecialType.Tag => Icon.Tag,
|
||
|
SpecialType.Prefab => Icon.Cube,
|
||
|
_ => null,
|
||
|
};
|
||
|
|
||
|
public Color? DisplayColor => DocColor ?? Type switch {
|
||
|
SpecialType.Module => Color.FromRGB(1.0f, 0.9f, 0.7f),
|
||
|
SpecialType.System => Color.FromRGB(1.0f, 0.7f, 0.7f),
|
||
|
SpecialType.Relation => Color.FromRGB(0.7f, 1.0f, 0.8f),
|
||
|
SpecialType.Component => Color.FromRGB(0.6f, 0.6f, 1.0f),
|
||
|
SpecialType.Tag => Color.FromRGB(0.7f, 0.8f, 1.0f),
|
||
|
SpecialType.Prefab => Color.FromRGB(0.9f, 0.8f, 1.0f),
|
||
|
_ => null,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
public enum SpecialType
|
||
|
{
|
||
|
Module,
|
||
|
System,
|
||
|
Relation,
|
||
|
Component,
|
||
|
Tag,
|
||
|
Prefab,
|
||
|
}
|
||
|
|
||
|
private const int MAX_CHILDREN = 64;
|
||
|
private void ExplorerView(EntityRef window, History? history, EntityRef? selected)
|
||
|
{
|
||
|
var Expanded = window.World.LookupByTypeOrThrow<Expanded>();
|
||
|
|
||
|
List<EntitySummary> GetSummaries(Entity? parent) {
|
||
|
var result = new List<EntitySummary>();
|
||
|
var expression = $"(ChildOf, {parent?.Id ?? 0})" // Must be child of parent, or root entity.
|
||
|
+ ",?(Identifier, Name)" // Name (in hierarchy)
|
||
|
+ ",?(flecs.doc.Description, Name)" // DocName (human-readable)
|
||
|
+ ",?(flecs.doc.Description, flecs.doc.Color)" // DocColor
|
||
|
+ ",[none] ?ChildOf(_, $This)" // HasChildren
|
||
|
+ $",?{Expanded.Id}({window.Id}, $This)" // IsExpanded
|
||
|
+ ",?Disabled" // IsDisabled
|
||
|
+ ",?Module,?flecs.system.System,?gaemstone.Doc.Relation,?Component,?Tag,?Prefab"; // Type
|
||
|
|
||
|
using (var rule = new Rule(window.World, new(expression))) {
|
||
|
foreach (var iter in rule.Iter()) {
|
||
|
var names = iter.FieldOrEmpty<Core.Identifier>(2);
|
||
|
var docNames = iter.FieldOrEmpty<Flecs.Doc.Description>(3);
|
||
|
var docColors = iter.FieldOrEmpty<Flecs.Doc.Description>(4);
|
||
|
|
||
|
var hasChildren = iter.FieldIsSet(5);
|
||
|
var isExpanded = iter.FieldIsSet(6);
|
||
|
var isDisabled = iter.FieldIsSet(7);
|
||
|
|
||
|
var isModule = iter.FieldIsSet(8);
|
||
|
var isSystem = iter.FieldIsSet(9);
|
||
|
var isRelation = iter.FieldIsSet(10);
|
||
|
var components = iter.FieldOrEmpty<Core.Component>(11);
|
||
|
var isTag = iter.FieldIsSet(12);
|
||
|
var isPrefab = iter.FieldIsSet(13);
|
||
|
|
||
|
for (var i = 0; i < iter.Count; i++) {
|
||
|
// Certain built-in components in Flecs actually have a size of 0,
|
||
|
// thus don't actually hold any value and behave more like tags.
|
||
|
// We pretend they are just tags and mark them as such.
|
||
|
var component = components.GetOrNull(i);
|
||
|
var isComponent = (component?.Size > 0);
|
||
|
var isTagEquiv = (component?.Size == 0) || isTag;
|
||
|
|
||
|
var type = isModule ? SpecialType.Module
|
||
|
: isSystem ? SpecialType.System
|
||
|
: isRelation ? SpecialType.Relation
|
||
|
: isComponent ? SpecialType.Component
|
||
|
: isTagEquiv ? SpecialType.Tag
|
||
|
: isPrefab ? SpecialType.Prefab
|
||
|
: (SpecialType?)null;
|
||
|
result.Add(new() {
|
||
|
Entity = iter.Entity(i),
|
||
|
Type = type,
|
||
|
Name = names.GetOrNull(i)?.ToString(),
|
||
|
DocName = docNames.GetOrNull(i)?.ToString(),
|
||
|
DocColor = Color.TryParseHex(docColors.GetOrNull(i)?.ToString()),
|
||
|
HasChildren = hasChildren,
|
||
|
IsExpanded = isExpanded,
|
||
|
IsDisabled = isDisabled,
|
||
|
});
|
||
|
if (result.Count > MAX_CHILDREN)
|
||
|
return result;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
result.Sort();
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
void EntryNode(EntitySummary summary) {
|
||
|
var entity = new EntityRef(window.World, summary.Entity);
|
||
|
var isExpanded = summary.IsExpanded;
|
||
|
var isSelected = (selected == entity);
|
||
|
|
||
|
var flags = ImGuiTreeNodeFlags.OpenOnArrow
|
||
|
| ImGuiTreeNodeFlags.OpenOnDoubleClick
|
||
|
| ImGuiTreeNodeFlags.SpanAvailWidth;
|
||
|
if (!summary.HasChildren) flags |= ImGuiTreeNodeFlags.Leaf
|
||
|
| ImGuiTreeNodeFlags.Bullet
|
||
|
| ImGuiTreeNodeFlags.NoTreePushOnOpen;
|
||
|
if (isSelected) flags |= ImGuiTreeNodeFlags.Selected;
|
||
|
|
||
|
var hasColor = false;
|
||
|
if (summary.DisplayColor is Color color) { ImGui.PushStyleColor(ImGuiCol.Text, color.RGBA); hasColor = true; }
|
||
|
if (summary.DocName != null) ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[2]);
|
||
|
if (summary.IsDisabled) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().DisabledAlpha);
|
||
|
ImGui.SetNextItemOpen(isExpanded);
|
||
|
ImGui.TreeNodeEx(summary.DisplayName, flags);
|
||
|
if (summary.IsDisabled) ImGui.PopStyleVar();
|
||
|
if (summary.DocName != null) ImGui.PopFont();
|
||
|
if (hasColor) ImGui.PopStyleColor();
|
||
|
|
||
|
// When hovering over the node, display brief description (if available).
|
||
|
if (ImGui.IsItemHovered() && entity.GetDocBrief() is string brief)
|
||
|
ImGui.SetTooltip(brief);
|
||
|
|
||
|
// When node is clicked (but not on the arrow), select this entity.
|
||
|
if (ImGui.IsItemClicked() && !ImGui.IsItemToggledOpen())
|
||
|
SetSelected(window, history, entity, scrollTo: false);
|
||
|
|
||
|
// When node is toggled, toggle (Expanded, entity)
|
||
|
// relation on the inspector window entity.
|
||
|
if (ImGui.IsItemToggledOpen()) {
|
||
|
if (isExpanded) {
|
||
|
isExpanded = false;
|
||
|
window.Remove(Expanded, entity);
|
||
|
} else if (summary.HasChildren) {
|
||
|
isExpanded = true;
|
||
|
window.Add(Expanded, entity);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (window.Has<ScrollToSelected>() && isSelected) {
|
||
|
ImGui.SetScrollHereY();
|
||
|
window.Remove<ScrollToSelected>();
|
||
|
}
|
||
|
|
||
|
if (isExpanded && summary.HasChildren) {
|
||
|
var children = GetSummaries(entity);
|
||
|
if (children.Count > MAX_CHILDREN) {
|
||
|
ImGui.TreePush();
|
||
|
ImGui.TextWrapped($"{Icon.ExclamationTriangle} Too many children. " +
|
||
|
"If an entity's full path is known, it can be entered in the path input.");
|
||
|
ImGui.TreePop();
|
||
|
} else foreach (var child in children)
|
||
|
EntryNode(child);
|
||
|
ImGui.TreePop();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
foreach (var summary in GetSummaries(Entity.None))
|
||
|
EntryNode(summary);
|
||
|
}
|
||
|
|
||
|
private void ComponentsTab(EntityRef window, History? history, EntityRef? selected)
|
||
|
{
|
||
|
if (selected == null) return;
|
||
|
var ChildOf = window.World.LookupByTypeOrThrow<Core.ChildOf>();
|
||
|
foreach (var id in selected.Type) {
|
||
|
// Hide ChildOf relations, as they are visible in the explorer.
|
||
|
if (id.IsPair && (id.Id.RelationUnsafe == ChildOf)) continue;
|
||
|
RenderIdentifier(window, history, id);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void ReferencesTab(EntityRef window, History? history, EntityRef? selected)
|
||
|
{
|
||
|
if (selected == null) return;
|
||
|
var world = window.World;
|
||
|
var ChildOf = world.LookupByTypeOrThrow<Core.ChildOf>();
|
||
|
var Wildcard = world.LookupByTypeOrThrow<Core.Wildcard>();
|
||
|
|
||
|
if (ImGui.CollapsingHeader($"As {Icon.Tag} Entity", ImGuiTreeNodeFlags.DefaultOpen))
|
||
|
foreach (var iter in Iterator.FromTerm(world, new(selected)))
|
||
|
for (var i = 0; i < iter.Count; i++)
|
||
|
RenderEntity(window, history, iter.Entity(i));
|
||
|
|
||
|
if (ImGui.CollapsingHeader($"As {Icon.ShareAlt} Relation", ImGuiTreeNodeFlags.DefaultOpen))
|
||
|
foreach (var iter in Iterator.FromTerm(world, new(selected, Wildcard))) {
|
||
|
var id = iter.FieldId(1);
|
||
|
if (id.AsPair() is not (EntityRef relation, EntityRef target)) throw new InvalidOperationException();
|
||
|
if (relation == ChildOf) continue; // Hide ChildOf relations.
|
||
|
|
||
|
for (var i = 0; i < iter.Count; i++) {
|
||
|
RenderEntity(window, history, iter.Entity(i));
|
||
|
ImGui.SameLine(); ImGui.TextUnformatted("has"); ImGui.SameLine();
|
||
|
RenderIdentifier(window, history, id);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (ImGui.CollapsingHeader($"As {Icon.Bullseye} Target", ImGuiTreeNodeFlags.DefaultOpen))
|
||
|
foreach (var iter in Iterator.FromTerm(world, new(Wildcard, selected))) {
|
||
|
var id = iter.FieldId(1);
|
||
|
if (id.AsPair() is not (EntityRef relation, EntityRef target)) throw new InvalidOperationException();
|
||
|
if (relation == ChildOf) continue; // Hide ChildOf relations.
|
||
|
|
||
|
for (var i = 0; i < iter.Count; i++) {
|
||
|
RenderEntity(window, history, iter.Entity(i));
|
||
|
ImGui.SameLine(); ImGui.TextUnformatted("has"); ImGui.SameLine();
|
||
|
RenderIdentifier(window, history, id);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void DocumentationTab(EntityRef _1, History? _2, EntityRef? selected)
|
||
|
{
|
||
|
var hasSelected = (selected != null);
|
||
|
|
||
|
ImGui.BeginTable("Documentation", 2);
|
||
|
ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed);
|
||
|
ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.WidthStretch);
|
||
|
|
||
|
static void Column(string label, string? tooltip, bool fill = true) {
|
||
|
ImGui.TableNextColumn();
|
||
|
ImGui.AlignTextToFramePadding();
|
||
|
ImGui.TextUnformatted(label);
|
||
|
if (ImGui.IsItemHovered() && (tooltip != null))
|
||
|
ImGui.SetTooltip(tooltip);
|
||
|
ImGui.TableNextColumn();
|
||
|
if (fill) ImGui.SetNextItemWidth(-float.Epsilon);
|
||
|
}
|
||
|
|
||
|
Column($"{Icon.Tag} Display Name", """
|
||
|
A display name for this entity.
|
||
|
Names in the entity hierarchy must be unique within the parent entity,
|
||
|
This doesn't apply to display names - they are mostly informational.
|
||
|
""");
|
||
|
if (!hasSelected) ImGui.BeginDisabled();
|
||
|
var name = selected?.GetDocName(false) ?? "";
|
||
|
if (ImGui.InputText("##Name", ref name, 256))
|
||
|
selected!.SetDocName((name.Length > 0) ? name : null);
|
||
|
if (!hasSelected) ImGui.EndDisabled();
|
||
|
|
||
|
Column($"{Icon.Comment} Description",
|
||
|
"A brief description of this entity.");
|
||
|
if (!hasSelected) ImGui.BeginDisabled();
|
||
|
var brief = selected?.GetDocBrief() ?? "";
|
||
|
if (ImGui.InputText("##Brief", ref brief, 256))
|
||
|
selected!.SetDocBrief((brief.Length > 0) ? brief : null);
|
||
|
if (!hasSelected) ImGui.EndDisabled();
|
||
|
|
||
|
Column($"{Icon.FileText} Documentation", """
|
||
|
A detailed description, or full documentation, of this entity's purpose and behaviors.
|
||
|
It's encouraged to use multiple paragraphs and markdown formatting if necessary.
|
||
|
""");
|
||
|
var cellPadding = ImGui.GetStyle().CellPadding.Y;
|
||
|
var minHeight = ImGui.GetTextLineHeightWithSpacing() * 4;
|
||
|
var availHeight = ImGui.GetContentRegionAvail().Y
|
||
|
- (ImGui.GetFrameHeight() + cellPadding * 2) * 2 - cellPadding;
|
||
|
if (!hasSelected) ImGui.BeginDisabled();
|
||
|
var detail = selected?.GetDocDetail() ?? "";
|
||
|
// TODO: Needs wordwrap.
|
||
|
if (ImGui.InputTextMultiline("##Detail", ref detail, 2048,
|
||
|
new(-float.Epsilon, Math.Max(minHeight, availHeight))))
|
||
|
selected!.SetDocDetail((detail.Length > 0) ? detail : null);
|
||
|
if (!hasSelected) ImGui.EndDisabled();
|
||
|
|
||
|
Column($"{Icon.Link} Link", """
|
||
|
A link to a website relating to this entity, such as
|
||
|
a module's repository, or further documentation.
|
||
|
""");
|
||
|
if (!hasSelected) ImGui.BeginDisabled();
|
||
|
var link = selected?.GetDocLink() ?? "";
|
||
|
if (ImGui.InputText("##Link", ref link, 256))
|
||
|
selected!.SetDocLink((link.Length > 0) ? link : null);
|
||
|
if (!hasSelected) ImGui.EndDisabled();
|
||
|
|
||
|
Column($"{Icon.PaintBrush} Color", """
|
||
|
A custom color to represent this entity.
|
||
|
Used in the entity inspector's explorer view.
|
||
|
""", false);
|
||
|
if (!hasSelected) ImGui.BeginDisabled();
|
||
|
var maybeColor = Color.TryParseHex(selected?.GetDocColor());
|
||
|
var hasColor = (maybeColor != null);
|
||
|
var color = maybeColor ?? Color.White;
|
||
|
if (ImGui.Checkbox("##HasColor", ref hasColor)) {
|
||
|
if (hasColor) selected!.SetDocColor(color.ToHexString());
|
||
|
else selected!.SetDocColor(null);
|
||
|
}
|
||
|
ImGui.SameLine();
|
||
|
if (!hasColor) ImGui.BeginDisabled();
|
||
|
ImGui.SetNextItemWidth(-float.Epsilon);
|
||
|
var colorVec = color.ToVector3();
|
||
|
if (ImGui.ColorEdit3("##Color", ref colorVec))
|
||
|
selected!.SetDocColor(Color.FromRGB(colorVec).ToHexString());
|
||
|
if (!hasColor) ImGui.EndDisabled();
|
||
|
if (!hasSelected) ImGui.EndDisabled();
|
||
|
|
||
|
ImGui.EndTable();
|
||
|
ImGui.EndTabItem();
|
||
|
}
|
||
|
|
||
|
|
||
|
// =======================
|
||
|
// == Utility Functions ==
|
||
|
// =======================
|
||
|
|
||
|
private EntityRef NewEntityInspectorWindow(World world)
|
||
|
=> world.New().Add<InspectorWindow>().Set(new History())
|
||
|
.Build().SetDocName("Entity Inspector");
|
||
|
|
||
|
private void SetSelected(
|
||
|
EntityRef window, // The InspectorWindow entity.
|
||
|
History? history, // InspectorWindow's History component, null if it shouldn't be changed.
|
||
|
EntityRef? entity, // Entity to set as selected or null to unset.
|
||
|
bool scrollTo = true) // Should entity be scrolled to in the explorer view?
|
||
|
{
|
||
|
if (entity != null) window.Add<Selected>(entity);
|
||
|
else window.Remove<Selected, Core.Wildcard>();
|
||
|
|
||
|
for (var parent = entity?.Parent; parent != null; parent = parent.Parent)
|
||
|
window.Add<Expanded>(parent);
|
||
|
|
||
|
if ((entity != null) && scrollTo)
|
||
|
window.Add<ScrollToSelected>();
|
||
|
|
||
|
if (history != null) {
|
||
|
if (entity != null) history.Current = new History.Entry(entity, history.Current, null);
|
||
|
else if (history.Current is History.Entry entry) entry.Next = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void GoToPrevious(EntityRef window, History history, EntityRef? selected)
|
||
|
{
|
||
|
if (selected != null) {
|
||
|
if (history.Current?.Prev == null) return;
|
||
|
history.Current = history.Current.Prev;
|
||
|
} else if (history.Current == null) return;
|
||
|
|
||
|
var entity = EntityRef.CreateOrNull(window.World, history.Current.Entity);
|
||
|
SetSelected(window, null, entity);
|
||
|
// TODO: Set path if entity could not be found.
|
||
|
}
|
||
|
|
||
|
private void GoToNext(EntityRef window, History history)
|
||
|
{
|
||
|
if (history.Current?.Next == null) return;
|
||
|
history.Current = history.Current.Next;
|
||
|
|
||
|
var entity = EntityRef.CreateOrNull(window.World, history.Current.Entity);
|
||
|
SetSelected(window, null, entity);
|
||
|
// TODO: Set path if entity could not be found.
|
||
|
}
|
||
|
|
||
|
private Rule? _findDisplayTypeRule;
|
||
|
private EntityRef? FindDisplayType(EntityRef entity)
|
||
|
{
|
||
|
var world = entity.World;
|
||
|
var component = world.LookupByTypeOrThrow<Core.Component>();
|
||
|
|
||
|
var rule = _findDisplayTypeRule ??= new Rule(world, new(
|
||
|
$"$Type, gaemstone.Doc.DisplayType($Type)"));
|
||
|
var typeVar = rule.Variables["Type"]!;
|
||
|
|
||
|
var curType = (EntityRef?)null;
|
||
|
var curPriority = float.MaxValue;
|
||
|
foreach (var iter in _findDisplayTypeRule.Iter().SetVar(rule.ThisVar!, entity))
|
||
|
for (var i = 0; i < iter.Count; i++) {
|
||
|
var type = iter.GetVar(typeVar);
|
||
|
if ((type == component) && (entity.GetOrNull<Core.Component>(component)?.Size == 0))
|
||
|
type = world.LookupByTypeOrThrow<Core.Tag>();
|
||
|
var priority = type.GetOrNull<DocPriority>()?.Value ?? float.MaxValue;
|
||
|
if (priority <= curPriority) { curType = type; curPriority = priority; }
|
||
|
}
|
||
|
|
||
|
return curType;
|
||
|
}
|
||
|
|
||
|
|
||
|
// =============================
|
||
|
// == Utility ImGui Functions ==
|
||
|
// =============================
|
||
|
|
||
|
private void RenderIdentifier(EntityRef window, History? history, IdentifierRef id)
|
||
|
{
|
||
|
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, ImGui.GetStyle().ItemSpacing.Y));
|
||
|
if (id.AsPair() is (EntityRef relation, EntityRef target)) {
|
||
|
ImGui.TextUnformatted("("); ImGui.SameLine();
|
||
|
RenderEntity(window, history, relation);
|
||
|
ImGui.SameLine(); ImGui.TextUnformatted(","); ImGui.SameLine();
|
||
|
RenderEntity(window, history, target);
|
||
|
ImGui.SameLine(); ImGui.TextUnformatted(")");
|
||
|
} else if (id.AsEntity() is EntityRef entity)
|
||
|
RenderEntity(window, history, entity);
|
||
|
else
|
||
|
ImGui.TextUnformatted(id.ToString());
|
||
|
ImGui.PopStyleVar();
|
||
|
}
|
||
|
|
||
|
private void RenderEntity(EntityRef window, History? history, EntityRef entity)
|
||
|
{
|
||
|
var pos = ImGui.GetCursorScreenPos();
|
||
|
// Adjust based on AlignTextToFramePadding() or similar.
|
||
|
pos.Y += ImGuiInternal.GetCurrentWindow().DC.CurrLineTextBaseOffset;
|
||
|
|
||
|
// TODO: Calculate the size properly.
|
||
|
var dummySize = new Vector2(20, ImGui.GetTextLineHeight());
|
||
|
if (!ImGui.IsRectVisible(pos, pos + dummySize)) { ImGui.Dummy(dummySize); return; }
|
||
|
|
||
|
var displayType = FindDisplayType(entity);
|
||
|
var docColor = Color.TryParseHex(entity.GetDocColor()) ?? Color.TryParseHex(displayType?.GetDocColor());
|
||
|
var docIcon = entity.GetOrNull<DocIcon>()?.Value.ToString() ?? displayType?.GetOrNull<DocIcon>()?.Value.ToString();
|
||
|
var docName = entity.GetDocName(false);
|
||
|
|
||
|
var isDisabled = entity.IsDisabled;
|
||
|
var displayName = (docName != null) ? $"\"{docName}\"" : entity.Name ?? entity.Id.ToString();
|
||
|
if (docIcon is string icon) displayName = $"{icon} {displayName}";
|
||
|
|
||
|
var font = ImGui.GetIO().Fonts.Fonts[(docName != null) ? 2 : 0];
|
||
|
ImGui.PushFont(font); var size = ImGui.CalcTextSize(displayName); ImGui.PopFont();
|
||
|
var color = docColor ?? Color.FromRGBA(ImGui.GetColorU32(ImGuiCol.Text));
|
||
|
if (isDisabled) color = color.WithAlpha(ImGui.GetStyle().DisabledAlpha);
|
||
|
|
||
|
var ctrl = ImGui.IsKeyDown(ImGuiKey.ModCtrl);
|
||
|
var shift = ImGui.IsKeyDown(ImGuiKey.ModShift);
|
||
|
if (ImGui.InvisibleButton(entity.Id.ToString(), size) && (ctrl || shift)) {
|
||
|
if (shift) window = NewEntityInspectorWindow(window.World);
|
||
|
SetSelected(window, history, entity);
|
||
|
}
|
||
|
|
||
|
var drawList = ImGui.GetWindowDrawList();
|
||
|
drawList.AddText(font, ImGui.GetFontSize(), pos, color.RGBA, displayName);
|
||
|
// Draw an underscore (like a hyperlink) if hovered and Ctrl key is held.
|
||
|
if (ImGui.IsItemHovered() && (ctrl || shift)) {
|
||
|
pos.Y -= 1.75f;
|
||
|
drawList.AddLine(pos + new Vector2(0, size.Y), pos + size, color.RGBA);
|
||
|
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
|
||
|
}
|
||
|
|
||
|
if (ImGui.IsItemHovered()) {
|
||
|
ImGui.BeginTooltip();
|
||
|
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]);
|
||
|
ImGui.TextUnformatted(entity.GetFullPath().ToString());
|
||
|
ImGui.PopFont();
|
||
|
if (isDisabled) {
|
||
|
ImGui.SameLine();
|
||
|
ImGui.BeginDisabled();
|
||
|
ImGui.TextUnformatted(" (disabled)");
|
||
|
ImGui.EndDisabled();
|
||
|
}
|
||
|
if (entity.GetDocBrief() is string brief) ImGui.Text(brief);
|
||
|
ImGui.EndTooltip();
|
||
|
}
|
||
|
}
|
||
|
}
|