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.
 
 

763 lines
28 KiB

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();
}
}
}