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.
766 lines
28 KiB
766 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 ImGuiNET; |
|
using static gaemstone.Client.Systems.ImGuiManager; |
|
using static gaemstone.Flecs.Core; |
|
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; } |
|
|
|
|
|
private const string DefaultWindowTitle = "Inspector Gadget"; |
|
|
|
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/Component" , 4 , Icon.PencilSquare , 0.6f, 0.6f, 1.0f); |
|
SetDocInfo("/flecs/core/Tag" , 5 , Icon.Tag , 0.7f, 0.8f, 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, DefaultWindowTitle, hasAnyInspector)) |
|
NewEntityInspectorWindow(world); |
|
} |
|
|
|
[System] |
|
public void ShowInspectorWindow(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]); |
|
var title = window.GetDocName() ?? DefaultWindowTitle; |
|
if (ImGui.Begin($"{Icon.Search} {title}###{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, Wildcard>(); |
|
if (IconButtonWithToolTip(Icon.Outdent, "Collapse all items in the Explorer View", hasExpanded)) |
|
window.Remove<Expanded, 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))) { |
|
// TODO: Delete history for deleted entity? |
|
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 class ExplorerEntry |
|
: IComparable<ExplorerEntry> |
|
{ |
|
private readonly EntityInspector _module; |
|
|
|
public EntityRef Entity { get; } |
|
public int NumChildren { get; } |
|
public bool HasChildren => (NumChildren > 0); |
|
public bool IsExpanded { get; } |
|
public bool IsDisabled { get; } |
|
|
|
private int _nameCached; private string? _name; |
|
private int _docNameCached; private string? _docName; |
|
public string? Name => (_nameCached++ > 0) ? _name : _name = Entity.Name; |
|
public string? DocName => (_docNameCached++ > 0) ? _docName : _docName = Entity.GetDocName(); |
|
|
|
private float? _priority; |
|
private EntityRef? _type; |
|
public float Priority => EnsureTypeCached()._priority!.Value; |
|
public EntityRef? Type => EnsureTypeCached()._type; |
|
|
|
public ExplorerEntry(EntityInspector module, EntityRef entity, |
|
int numChildren, bool isExpanded, bool isDisabled) |
|
{ |
|
_module = module; |
|
Entity = entity; |
|
NumChildren = numChildren; |
|
IsExpanded = (isExpanded && HasChildren); |
|
IsDisabled = isDisabled; |
|
} |
|
|
|
private ExplorerEntry EnsureTypeCached() |
|
{ |
|
if (_priority != null) return this; |
|
(_type, _priority) = _module.FindDisplayType(Entity); |
|
return this; |
|
} |
|
|
|
public int CompareTo(ExplorerEntry? other) |
|
{ |
|
if (other == null) return -1; |
|
return Compare(Priority, other.Priority) |
|
?? Compare(Name, other.Name) |
|
?? Compare(DocName, other.DocName) |
|
?? Compare(Entity.Id, other.Entity.Id) |
|
?? 0; |
|
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; |
|
} |
|
} |
|
} |
|
|
|
private void ExplorerView(EntityRef window, History? history, EntityRef? selected) |
|
{ |
|
// For some reason, the analyzer thinks world can be |
|
// nullable, so let's be explicit about the type here. |
|
World world = window.World; |
|
|
|
var Wildcard = world.LookupByTypeOrThrow<Wildcard>().Entity; |
|
var Any = world.LookupByTypeOrThrow<Any>().Entity; |
|
var This = world.LookupByTypeOrThrow<This>().Entity; |
|
var Var = world.LookupByTypeOrThrow<Variable>().Entity; |
|
bool IsSpecialEntity(Entity entity) |
|
=> (entity == Wildcard) || (entity == Any) |
|
|| (entity == This) || (entity == Var); |
|
|
|
var expId = world.LookupByTypeOrThrow<Expanded>().Id; |
|
List<ExplorerEntry> GetEntries(Entity? parent) { |
|
var result = new List<ExplorerEntry>(); |
|
using var rule = new Rule(world, new( |
|
$"(ChildOf, {parent?.Id ?? 0})" // Must be child of parent, or root entity. |
|
+ $",?{expId}({window.Id}, $This)" // Whether entity is expanded in explorer view. |
|
+ $",?Disabled" // Don't filter out disabled entities. |
|
)); |
|
foreach (var iter in rule.Iter()) { |
|
var isExpanded = iter.FieldIsSet(2); |
|
var isDisabled = iter.FieldIsSet(3); |
|
for (var i = 0; i < iter.Count; i++) { |
|
var entity = iter.Entity(i); |
|
var count = IsSpecialEntity(entity) ? 0 |
|
: IdRef.Pair<ChildOf>(entity).Count; |
|
result.Add(new(this, entity, count, isExpanded, isDisabled)); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
var spacingX = ImGui.GetStyle().ItemInnerSpacing.X; |
|
var spacingY = ImGui.GetStyle().ItemSpacing.Y; |
|
var nodeHeight = ImGui.GetTextLineHeight(); |
|
|
|
var scrollToSelected = window.Has<ScrollToSelected>(); |
|
|
|
float GetIndent(int depth) => depth * (nodeHeight + spacingX); |
|
|
|
bool RenderNode(ExplorerEntry entry, int? depth) { |
|
var entity = entry.Entity; |
|
var startY = ImGui.GetCursorPosY(); |
|
var isVisible = ImGui.IsRectVisible(new(nodeHeight, nodeHeight)); |
|
var isSelected = (entity == selected); |
|
var isRenderPass = (depth != null); |
|
|
|
if (isRenderPass) { |
|
// Button for expanding the child entries. |
|
if (entry.HasChildren && !entry.IsExpanded) { |
|
ImGui.SetCursorPosX(GetIndent(depth!.Value)); |
|
if (ImGui.Button($"##Expand{entity.Id}", new(nodeHeight))) |
|
window.Add<Expanded>(entity); |
|
} |
|
|
|
// Render the entity text, or a dummy if not necessary. |
|
ImGui.SetCursorPos(new(GetIndent(depth!.Value + 1), startY)); |
|
var flags = RenderEntityFlags.IsHeaderLike |
|
| RenderEntityFlags.SpanAvailWidth; |
|
if (isSelected) flags |= RenderEntityFlags.IsSelected; |
|
if (isVisible) RenderEntity(window, history, entity, flags); |
|
else ImGui.Dummy(new(nodeHeight, nodeHeight)); |
|
|
|
// Scroll to this entry if selected and scrolling was requested. |
|
if (scrollToSelected && isSelected) { |
|
window.Remove<ScrollToSelected>(); |
|
ImGui.SetScrollHereY(); |
|
} |
|
} else ImGui.Dummy(new(nodeHeight, nodeHeight)); |
|
|
|
// If node is not expanded, don't run any of code involving child entries. |
|
if (!entry.IsExpanded) return isVisible; |
|
|
|
// Get the child entries of this entity. |
|
var entries = GetEntries(entity); |
|
|
|
// If scrolling to selected, make sure every node is sorted. |
|
// Otherwise the target node's position might not be right. |
|
if (!scrollToSelected) { |
|
foreach (var child in entries) |
|
if (RenderNode(child, null)) |
|
isVisible = true; |
|
if (!isVisible) return false; |
|
} |
|
|
|
if (!isRenderPass) return isVisible; |
|
|
|
entries.Sort(); |
|
ImGui.SetCursorPosY(startY + nodeHeight); |
|
foreach (var child in entries) |
|
RenderNode(child, depth + 1); |
|
|
|
var fullHeight = ImGui.GetCursorPosY() - startY - spacingY; |
|
ImGui.SetCursorPos(new(GetIndent(depth!.Value), startY)); |
|
if (ImGui.Button($"##Expand{entity.Id}", new(nodeHeight, fullHeight))) |
|
window.Remove<Expanded>(entity); |
|
|
|
return true; |
|
} |
|
|
|
var entries = GetEntries(null); |
|
entries.Sort(); |
|
foreach (var child in entries) |
|
RenderNode(child, 0); |
|
} |
|
|
|
private void ComponentsTab(EntityRef window, History? history, EntityRef? selected) |
|
{ |
|
if (selected == null) return; |
|
var ChildOf = window.World.LookupByTypeOrThrow<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<ChildOf>(); |
|
var Wildcard = world.LookupByTypeOrThrow<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(DefaultWindowTitle); |
|
|
|
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, 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? DisplayType, float Priority) FindDisplayType(EntityRef entity) |
|
{ |
|
var world = entity.World; |
|
var component = world.LookupByTypeOrThrow<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<Component>(component)?.Size == 0)) |
|
type = world.LookupByTypeOrThrow<Tag>(); |
|
var priority = type.GetOrNull<DocPriority>()?.Value ?? float.MaxValue; |
|
if (priority <= curPriority) { curType = type; curPriority = priority; } |
|
} |
|
|
|
return (curType, curPriority); |
|
} |
|
|
|
|
|
// ============================= |
|
// == Utility ImGui Functions == |
|
// ============================= |
|
|
|
private void RenderIdentifier(EntityRef window, History? history, IdRef 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, |
|
RenderEntityFlags flags = default) |
|
{ |
|
var spanAvailWidth = (flags & RenderEntityFlags.SpanAvailWidth) != 0; |
|
var isHeaderLike = (flags & RenderEntityFlags.IsHeaderLike) != 0; |
|
var isSelected = (flags & RenderEntityFlags.IsSelected) != 0; |
|
|
|
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]; |
|
var color = docColor ?? Color.FromRGBA(ImGui.GetColorU32(ImGuiCol.Text)); |
|
if (isDisabled) color = color.WithAlpha(ImGui.GetStyle().DisabledAlpha); |
|
|
|
// Gotta push the font to calculate size properly. |
|
// Can't just pass it to CalcTextSize for some reason. |
|
ImGui.PushFont(font); var size = ImGui.CalcTextSize(displayName); ImGui.PopFont(); |
|
if (isHeaderLike) size.X += ImGui.GetStyle().FramePadding.X * 2; |
|
if (spanAvailWidth) size.X = Math.Max(size.X, ImGui.GetContentRegionAvail().X); |
|
|
|
if (isHeaderLike) { |
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0); |
|
ImGui.PushStyleColor(ImGuiCol.Button, isSelected ? ImGui.GetColorU32(ImGuiCol.Header) |
|
: Color.Transparent.RGBA); |
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive , ImGui.GetColorU32(ImGuiCol.HeaderActive)); |
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.HeaderHovered)); |
|
ImGui.Button($"##{entity.Id}", size); |
|
ImGui.PopStyleColor(3); |
|
ImGui.PopStyleVar(); |
|
} else ImGui.InvisibleButton($"##{entity.Id}", size); |
|
|
|
var shift = ImGui.IsKeyDown(ImGuiKey.ModShift); |
|
var canClick = isHeaderLike || shift; |
|
if (canClick && ImGui.IsItemClicked()) { |
|
if (shift) window = NewEntityInspectorWindow(window.World); |
|
// Don't re-select this entity if it's already selected. |
|
// Unless we're opening it in a new inspector window. |
|
// This is to make sure the history doesn't get polluted. |
|
if (!isSelected || shift) SetSelected(window, history, entity, false); |
|
} |
|
|
|
var drawList = ImGui.GetWindowDrawList(); |
|
if (isHeaderLike) pos.X += ImGui.GetStyle().FramePadding.X; |
|
drawList.AddText(font, ImGui.GetFontSize(), pos, color.RGBA, displayName); |
|
if (!isHeaderLike && canClick && ImGui.IsItemHovered()) { |
|
// Draw a hyperlink-link underscore. |
|
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(); |
|
} |
|
} |
|
|
|
[Flags] |
|
private enum RenderEntityFlags |
|
{ |
|
SpanAvailWidth = 0b001, // Extend the bounding box to span available width. |
|
IsHeaderLike = 0b010, // Render the element like a header instead of a link. |
|
IsSelected = 0b100, // Render a background showing this as selected. |
|
} |
|
}
|
|
|