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.
 
 

785 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 Icon = gaemstone.Client.Utility.ForkAwesome;
using ImGuiInternal = ImGuiNET.Internal.ImGui;
namespace gaemstone.Client.Systems;
[Module]
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
public partial class EntityInspector
: IModuleInitializer
{
[Tag]
public struct InspectorWindow { }
[Relation, Exclusive]
[Add<Core.OnDeleteTarget, Core.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(Entity entity, EntityPath path, Entry? prev, Entry? next)
{
Entity = entity;
Path = path;
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 static void Initialize<T>(Entity<T> module)
{
void SetDocInfo(string path, float priority, string icon, float r, float g, float b)
=> module.World.LookupPathOrThrow(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);
// TODO: Handle tags like Flecs does.
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 static void ShowUIButton<T>(World<T> world, ImGuiData _)
{
var hasAnyInspector = false;
var inspectorWindow = world.Entity<InspectorWindow>();
foreach (var entity in Iterator<T>.FromTerm(world, new(inspectorWindow)))
{ hasAnyInspector = true; break; }
if (ImGuiUtility.UIButton(0, Icon.Search, DefaultWindowTitle, hasAnyInspector))
NewEntityInspectorWindow(world);
}
[System]
public static void ShowInspectorWindow<T>(Entity<T> 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.NumericId}",
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<Entity<T>, History?, Entity<T>?> 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<Core.OnRemove>]
public static void ClearStorageOnRemove<T>(Entity<T> _1, InspectorWindow _2)
{
// TODO: Clear out settings store for the window.
}
private static void ActionBarAndPath<T>(Entity<T> window, History? history, Entity<T>? selected)
{
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)))
SetSelected(window, history, selected?.NewChild().Build());
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 static void PathInput<T>(Entity<T> window, History? history, Entity<T>? selected, float availableWidth)
{
var style = ImGui.GetStyle();
ImGui.AlignTextToFramePadding();
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, style.ItemSpacing.Y));
var path = selected?.Path ?? 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])) {
var toSelect = window.World.LookupPathOrNull(path[..(actualIndex + 1)]);
SetSelected(window, history, toSelect);
}
ImGui.SameLine();
}
}
ImGui.Text("/"); ImGui.SameLine();
var name = path?.Name.ToString() ?? "";
ImGui.SetNextItemWidth(-float.Epsilon);
ImGui.InputText("##Path", ref name, 256);
ImGui.PopStyleVar();
}
private interface IExplorerEntry
: IComparable<IExplorerEntry>
{
public Entity Entity { get; }
public int NumChildren { get; }
public bool HasChildren { get; }
public bool IsExpanded { get; }
public bool IsDisabled { get; }
public string? Name { get; }
public string? DocName { get; }
public float Priority { get; }
public Entity? Type { get; }
}
private class ExplorerEntry<T>
: IExplorerEntry
{
public Entity<T> Entity { get; }
Entity IExplorerEntry.Entity => Entity;
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 Entity? _type;
public float Priority => EnsureTypeCached()._priority!.Value;
public Entity? Type => EnsureTypeCached()._type;
public ExplorerEntry(Entity<T> entity,
int numChildren, bool isExpanded, bool isDisabled)
{
Entity = entity;
NumChildren = numChildren;
IsExpanded = (isExpanded && HasChildren);
IsDisabled = isDisabled;
}
private ExplorerEntry<T> EnsureTypeCached()
{
if (_priority != null) return this;
(_type, _priority) = FindDisplayType(Entity);
return this;
}
public int CompareTo(IExplorerEntry? other)
{
if (other == null) return -1;
return Compare(Priority, other.Priority)
?? Compare(Name, other.Name)
?? Compare(DocName, other.DocName)
?? Compare(Entity.NumericId, other.Entity.NumericId)
?? 0;
static int? Compare<TCompare>(TCompare x, TCompare y)
{
if (x is null) { if (y is null) return null; else return 1; }
else if (y is null) return -1;
var result = Comparer<TCompare>.Default.Compare(x, y);
return (result != 0) ? result : null;
}
}
}
private static void ExplorerView<T>(Entity<T> window, History? history, Entity<T>? selected)
{
// For some reason, the analyzer thinks world can be
// nullable, so let's be explicit about the type here.
var world = window.World;
var Wildcard = world.Entity<Core.Wildcard>().Value;
var Any = world.Entity<Core.Any>().Value;
var This = world.Entity<Core.This>().Value;
var Variable = world.Entity<Core.Variable>().Value;
bool IsSpecialEntity(Entity entity)
=> (entity == Wildcard) || (entity == Any)
|| (entity == This) || (entity == Variable);
var expId = world.Entity<Expanded>().NumericId;
List<IExplorerEntry> GetEntries(Entity? parent) {
var result = new List<IExplorerEntry>();
using var rule = new Rule<T>(world, new(
$"(ChildOf, {parent?.NumericId ?? 0})" // Must be child of parent, or root entity.
+ $",?{expId}({window.NumericId}, $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
: world.Pair<Core.ChildOf>(entity).Count;
result.Add(new ExplorerEntry<T>(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(IExplorerEntry entry, int? depth) {
var entity = Entity<T>.GetOrThrow(world, 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.NumericId}", 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.NumericId}", new(nodeHeight, fullHeight)))
window.Remove<Expanded>(entity);
return true;
}
var entries = GetEntries(null);
entries.Sort();
foreach (var child in entries)
RenderNode(child, 0);
}
private static void ComponentsTab<T>(Entity<T> window, History? history, Entity<T>? sel)
{
if (sel is not Entity<T> selected) return;
var ChildOf = window.World.Entity<Core.ChildOf>();
foreach (var id in selected.Type) {
// Hide ChildOf relations, as they are visible in the explorer.
if (id.IsPair && (id.Value.RelationUnsafe == ChildOf)) continue;
RenderIdentifier(window, history, id);
}
}
private static void ReferencesTab<T>(Entity<T> window, History? history, Entity<T>? sel)
{
if (sel is not Entity<T> selected) return;
var world = window.World;
var ChildOf = world.Entity<Core.ChildOf>();
var Wildcard = world.Entity<Core.Wildcard>();
if (ImGui.CollapsingHeader($"As {Icon.Tag} Component", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in Iterator<T>.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<T>.FromTerm(world, new(selected, Wildcard))) {
var id = iter.FieldId(1);
if (id.AsPair() is not (Entity<T> relation, Entity<T> 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<T>.FromTerm(world, new(Wildcard, selected))) {
var id = iter.FieldId(1);
if (id.AsPair() is not (Entity<T> relation, Entity<T> 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 static void DocumentationTab<T>(Entity<T> _1, History? _2, Entity<T>? 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 static Entity<T> NewEntityInspectorWindow<T>(World<T> world)
=> world.New().Add<InspectorWindow>().Set(new History())
.Build().SetDocName(DefaultWindowTitle);
private static void SetSelected<T>(
Entity<T> window, // The InspectorWindow entity.
History? history, // InspectorWindow's History component, null if it shouldn't be changed.
Entity<T>? entity, // Entity to set as selected or null to unset.
bool scrollTo = true) // Should entity be scrolled to in the explorer view?
{
if (entity is Entity<T> e1) window.Add<Selected>(e1);
else window.Remove<Selected, Core.Wildcard>();
for (var p = entity?.Parent; p is Entity<T> parent; p = parent.Parent)
window.Add<Expanded>(parent);
if ((entity != null) && scrollTo)
window.Add<ScrollToSelected>();
if (history != null) {
if (entity is Entity<T> e2) history.Current = new(e2, e2.Path, history.Current, null);
else if (history.Current is History.Entry entry) entry.Next = null;
}
}
private static void GoToPrevious<T>(Entity<T> window, History history, Entity<T>? selected)
{
if (selected != null) {
if (history.Current?.Prev == null) return;
history.Current = history.Current.Prev;
} else if (history.Current == null) return;
var entity = Entity<T>.GetOrNull(window.World, history.Current.Entity);
SetSelected(window, null, entity);
// TODO: Set path if entity could not be found.
}
private static void GoToNext<T>(Entity<T> window, History history)
{
if (history.Current?.Next == null) return;
history.Current = history.Current.Next;
var entity = Entity<T>.GetOrNull(window.World, history.Current.Entity);
SetSelected(window, null, entity);
// TODO: Set path if entity could not be found.
}
private static object? _findDisplayTypeRule;
private static (Entity<T>? DisplayType, float Priority) FindDisplayType<T>(Entity<T> entity)
{
var world = entity.World;
var component = world.Entity<Core.Component>();
var rule = (Rule<T>)(_findDisplayTypeRule ??= new Rule<T>(world, new(
$"$Type, gaemstone.Doc.DisplayType($Type)")));
var typeVar = rule.Variables["Type"]!;
var curType = (Entity<T>?)null;
var curPriority = float.MaxValue;
foreach (var iter in rule.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.Entity<Core.Tag>();
var priority = type?.GetOrNull<DocPriority>()?.Value ?? float.MaxValue;
if (priority <= curPriority) { curType = type; curPriority = priority; }
}
return (curType, curPriority);
}
// =============================
// == Utility ImGui Functions ==
// =============================
private static void RenderIdentifier<T>(Entity<T> window, History? history, Id<T> id)
{
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, ImGui.GetStyle().ItemSpacing.Y));
if (id.AsPair() is (Entity<T> relation, Entity<T> 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 Entity<T> entity)
RenderEntity(window, history, entity);
else
ImGui.TextUnformatted(id.ToString());
ImGui.PopStyleVar();
}
private static void RenderEntity<T>(
Entity<T> window, History? history, Entity<T> 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.NumericId.ToString();
if (docIcon != null) displayName = $"{docIcon} {displayName}";
// Gotta push the font to calculate size properly.
// Can't just pass it to CalcTextSize for some reason.
ImGui.PushFont(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);
var size = ImGui.CalcTextSize(displayName);
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.NumericId}", size);
ImGui.PopStyleColor(3);
ImGui.PopStyleVar();
} else ImGui.InvisibleButton($"##{entity.NumericId}", size);
var shift = ImGui.IsKeyDown(ImGuiKey.ModShift);
var hovered = ImGui.IsItemHovered();
var canClick = isHeaderLike || hovered || 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(ImGui.GetFont(), ImGui.GetFontSize(), pos, color.RGBA, displayName);
if (!isHeaderLike && canClick && hovered) {
// Draw a hyperlink-link underscore.
var p1 = pos + new Vector2( 0, size.Y - 1.75f);
var p2 = pos + new Vector2(size.X, size.Y - 1.75f);
if (docIcon != null) p1.X += ImGui.CalcTextSize($"{docIcon} ").X;
drawList.AddLine(p1, p2, color.RGBA);
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
}
if (hovered) {
ImGui.BeginTooltip();
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]);
ImGui.TextUnformatted(entity.Path.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();
}
ImGui.PopFont();
}
[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.
}
}