diff --git a/plugins/engine.editor/Engine.Editor/EditorPlugin.cs b/plugins/engine.editor/Engine.Editor/EditorPlugin.cs
index 5312b5d..4604cd1 100644
--- a/plugins/engine.editor/Engine.Editor/EditorPlugin.cs
+++ b/plugins/engine.editor/Engine.Editor/EditorPlugin.cs
@@ -19,14 +19,15 @@ namespace Engine.Editor;
/// split exists at all.
///
/// Nothing here reads or writes a Component through GetComponent/Query —
-/// the Hierarchy and Inspector panels (not built yet) will walk
-/// IWorld.Roots and GameObject.Components directly instead, which is why
-/// this plugin never needs to declare Reads/Writes on its system: those
-/// checks only guard the typed accessors, not plain property reads. See
-/// GameObject.AddComponent's own doc comment on the same gap.
+/// HierarchyPanel walks IWorld.Roots/GameObject.Children directly, and the
+/// not-yet-built Inspector will walk GameObject.Components the same way —
+/// which is why this plugin never needs to declare Reads/Writes on its
+/// system: those checks only guard the typed accessors, not plain property
+/// reads. See GameObject.AddComponent's own doc comment on the same gap.
///
public sealed class EditorPlugin : IPlugin
{
+ private readonly EditorState _state = new();
private GL? _gl;
private ImGuiController? _controller;
private ITime? _time;
@@ -64,6 +65,8 @@ public sealed class EditorPlugin : IPlugin
ImGui.Text($"Frame: {_time.FrameCount}");
ImGui.End();
+ HierarchyPanel.Draw(world, _state);
+
_controller.Render();
}
}
diff --git a/plugins/engine.editor/Engine.Editor/EditorState.cs b/plugins/engine.editor/Engine.Editor/EditorState.cs
new file mode 100644
index 0000000..59113e0
--- /dev/null
+++ b/plugins/engine.editor/Engine.Editor/EditorState.cs
@@ -0,0 +1,15 @@
+using Engine.Kernel.World;
+
+namespace Engine.Editor;
+
+///
+/// Shared, per-session editor state — currently just which GameObject the
+/// Hierarchy panel last clicked, so the Inspector panel (built against this
+/// same instance) knows what to show. One instance per EditorPlugin, not
+/// static: reloading engine.editor should start with nothing selected, not
+/// hold a reference to a GameObject that may not even exist anymore.
+///
+internal sealed class EditorState
+{
+ public GameObject? Selected { get; set; }
+}
diff --git a/plugins/engine.editor/Engine.Editor/HierarchyPanel.cs b/plugins/engine.editor/Engine.Editor/HierarchyPanel.cs
new file mode 100644
index 0000000..8609cee
--- /dev/null
+++ b/plugins/engine.editor/Engine.Editor/HierarchyPanel.cs
@@ -0,0 +1,50 @@
+using Engine.Kernel.World;
+using ImGuiNET;
+
+namespace Engine.Editor;
+
+///
+/// Walks IWorld.Roots/GameObject.Children directly — not Query<T>(),
+/// there's no component type to query for here — so, like EditorPlugin
+/// itself, this never touches SystemAccessScope and needs no declared
+/// Reads/Writes.
+///
+internal static class HierarchyPanel
+{
+ public static void Draw(IWorld world, EditorState state)
+ {
+ ImGui.Begin("Hierarchy");
+
+ foreach (var root in world.Roots)
+ DrawNode(root, state);
+
+ ImGui.End();
+ }
+
+ private static void DrawNode(GameObject go, EditorState state)
+ {
+ // PushID/PopID, not relying on go.Name for identity: sibling
+ // GameObjects can share a name (nothing stops it), and ImGui's
+ // default ID-from-label would then merge their open/selected state.
+ ImGui.PushID(go.GetHashCode());
+
+ var flags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.SpanAvailWidth;
+ if (go.Children.Count == 0)
+ flags |= ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen;
+ if (ReferenceEquals(state.Selected, go))
+ flags |= ImGuiTreeNodeFlags.Selected;
+
+ var open = ImGui.TreeNodeEx(go.Name, flags);
+ if (ImGui.IsItemClicked())
+ state.Selected = go;
+
+ if (open && go.Children.Count > 0)
+ {
+ foreach (var child in go.Children)
+ DrawNode(child, state);
+ ImGui.TreePop();
+ }
+
+ ImGui.PopID();
+ }
+}
diff --git a/samples/WindowDemo/scene.json b/samples/WindowDemo/scene.json
index 85d4edb..f104f17 100644
--- a/samples/WindowDemo/scene.json
+++ b/samples/WindowDemo/scene.json
@@ -9,6 +9,19 @@
"components": [
{ "type": "Engine.Render.Contracts.QuadRenderer, Engine.Render.Contracts", "data": {} }
],
- "children": []
+ "children": [
+ {
+ "name": "ChildQuad",
+ "transform": {
+ "position": [1.5, 0, 0],
+ "rotation": [0, 0, 0, 1],
+ "scale": [0.5, 0.5, 1]
+ },
+ "components": [
+ { "type": "Engine.Render.Contracts.QuadRenderer, Engine.Render.Contracts", "data": {} }
+ ],
+ "children": []
+ }
+ ]
}
]