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