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] public class EntityInspector : IModuleInitializer { [Tag] public struct InspectorWindow { } [Relation, Exclusive] [Add] 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() .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(); 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().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 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] 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(); if (IconButtonWithToolTip(Icon.Outdent, "Collapse all items in the Explorer View", hasExpanded)) window.Remove(); 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(); 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 { 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 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.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(); List GetSummaries(Entity? parent) { var result = new List(); 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(2); var docNames = iter.FieldOrEmpty(3); var docColors = iter.FieldOrEmpty(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(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() && isSelected) { ImGui.SetScrollHereY(); window.Remove(); } 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(); 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(); var Wildcard = world.LookupByTypeOrThrow(); 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().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(entity); else window.Remove(); for (var parent = entity?.Parent; parent != null; parent = parent.Parent) window.Add(parent); if ((entity != null) && scrollTo) window.Add(); 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(); 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(component)?.Size == 0)) type = world.LookupByTypeOrThrow(); var priority = type.GetOrNull()?.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()?.Value.ToString() ?? displayType?.GetOrNull()?.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(); } } }