diff --git a/AcDream.slnx b/AcDream.slnx
index 5a5029d..1cf8f24 100644
--- a/AcDream.slnx
+++ b/AcDream.slnx
@@ -7,6 +7,7 @@
+
diff --git a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj b/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj
new file mode 100644
index 0000000..65853fa
--- /dev/null
+++ b/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj
@@ -0,0 +1,23 @@
+
+
+ net10.0
+ enable
+ enable
+ latest
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs b/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs
new file mode 100644
index 0000000..70ba689
--- /dev/null
+++ b/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs
@@ -0,0 +1,62 @@
+using Hexa.NET.ImGui;
+using Hexa.NET.ImGui.Backends.OpenGL3;
+
+namespace AcDream.UI.ImGui;
+
+///
+/// One-shot ImGui setup / teardown for the devtools overlay. Called from
+/// GameWindow when ACDREAM_DEVTOOLS=1. Hides the cimgui
+/// context + OpenGL3 renderer-impl lifecycles behind two static methods
+/// so the calling code stays clean.
+///
+///
+/// Intentionally not an IDisposable singleton — the host
+/// window owns the one call to at application
+/// exit. Re-initialisation mid-session is not supported.
+///
+///
+public static class ImGuiBootstrapper
+{
+ private static bool _initialized;
+
+ ///
+ /// Create an ImGui context, apply the dark style + enable keyboard
+ /// navigation, and bootstrap the OpenGL3 renderer backend. The GL
+ /// context owned by Silk.NET must be current on the calling thread.
+ ///
+ ///
+ /// GLSL version directive for the ImGui-internal shader.
+ /// "#version 330" matches acdream's existing shaders and is
+ /// the safest default for the OpenGL 4.3 core profile we ship.
+ ///
+ public static void Initialize(string glslVersion = "#version 330")
+ {
+ if (_initialized) return;
+
+ Hexa.NET.ImGui.ImGui.CreateContext();
+ Hexa.NET.ImGui.ImGui.StyleColorsDark();
+
+ var io = Hexa.NET.ImGui.ImGui.GetIO();
+ io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard;
+ // DO NOT enable NavEnableGamepad — we don't wire a gamepad backend.
+ // DO NOT enable DockingEnable / ViewportsEnable — out of scope for D.2a.
+
+ ImGuiImplOpenGL3.Init(glslVersion);
+
+ _initialized = true;
+ }
+
+ /// Tear down the OpenGL3 renderer + destroy the ImGui context.
+ public static void Shutdown()
+ {
+ if (!_initialized) return;
+
+ ImGuiImplOpenGL3.Shutdown();
+ Hexa.NET.ImGui.ImGui.DestroyContext();
+
+ _initialized = false;
+ }
+
+ /// True after has run successfully.
+ public static bool IsInitialized => _initialized;
+}
diff --git a/src/AcDream.UI.ImGui/ImGuiPanelHost.cs b/src/AcDream.UI.ImGui/ImGuiPanelHost.cs
new file mode 100644
index 0000000..d9a3a4c
--- /dev/null
+++ b/src/AcDream.UI.ImGui/ImGuiPanelHost.cs
@@ -0,0 +1,46 @@
+using AcDream.UI.Abstractions;
+
+namespace AcDream.UI.ImGui;
+
+///
+/// implementation for the ImGui backend. Owns the
+/// registered panel set; iterates + draws every frame when the caller is
+/// inside an ImGui frame (between ImGui.NewFrame and
+/// ImGui.Render).
+///
+///
+/// This class does not call ImGui.NewFrame / ImGui.Render
+/// itself. Those belong to the caller (GameWindow) so GL-state
+/// ownership is explicit and the render-loop integration point is obvious.
+///
+///
+public sealed class ImGuiPanelHost : IPanelHost
+{
+ private readonly Dictionary _panels = new();
+ private readonly ImGuiPanelRenderer _renderer = new();
+
+ ///
+ public void Register(IPanel panel)
+ {
+ ArgumentNullException.ThrowIfNull(panel);
+ _panels[panel.Id] = panel; // idempotent by Id
+ }
+
+ ///
+ public void Unregister(string panelId) => _panels.Remove(panelId);
+
+ ///
+ public void RenderAll(PanelContext ctx)
+ {
+ // Order-independent — ImGui windows stack in the order they're drawn
+ // for focus purposes but we have <=1 panel in D.2a.
+ foreach (var panel in _panels.Values)
+ {
+ if (!panel.IsVisible) continue;
+ panel.Render(ctx, _renderer);
+ }
+ }
+
+ /// Current registered count (for diagnostics).
+ public int Count => _panels.Count;
+}
diff --git a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
new file mode 100644
index 0000000..17e463d
--- /dev/null
+++ b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
@@ -0,0 +1,41 @@
+using System.Numerics;
+using AcDream.UI.Abstractions;
+
+namespace AcDream.UI.ImGui;
+
+///
+/// implemented as thin wrappers around
+/// Hexa.NET.ImGui calls. This is the ONLY place where Hexa.NET.ImGui
+/// types appear outside of bootstrap / input-bridge plumbing — panels
+/// that need a feature must extend the abstraction here, not by importing
+/// ImGui in panel files.
+///
+public sealed class ImGuiPanelRenderer : IPanelRenderer
+{
+ ///
+ public bool Begin(string title) => Hexa.NET.ImGui.ImGui.Begin(title);
+
+ ///
+ public void End() => Hexa.NET.ImGui.ImGui.End();
+
+ ///
+ public void Text(string text) => Hexa.NET.ImGui.ImGui.TextUnformatted(text);
+
+ ///
+ public void SameLine() => Hexa.NET.ImGui.ImGui.SameLine();
+
+ ///
+ public void Separator() => Hexa.NET.ImGui.ImGui.Separator();
+
+ ///
+ public void ProgressBar(float fraction, float width, string? overlay = null)
+ {
+ // Clamp defensively; ImGui clamps internally but the abstraction
+ // contract promises to handle out-of-range values.
+ if (fraction < 0f) fraction = 0f;
+ else if (fraction > 1f) fraction = 1f;
+
+ var size = new Vector2(width, 0f); // height 0 → ImGui picks based on font
+ Hexa.NET.ImGui.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty);
+ }
+}
diff --git a/src/AcDream.UI.ImGui/SilkInputBridge.cs b/src/AcDream.UI.ImGui/SilkInputBridge.cs
new file mode 100644
index 0000000..6c47f28
--- /dev/null
+++ b/src/AcDream.UI.ImGui/SilkInputBridge.cs
@@ -0,0 +1,188 @@
+using System.Numerics;
+using Hexa.NET.ImGui;
+using Silk.NET.Input;
+
+namespace AcDream.UI.ImGui;
+
+///
+/// Forwards Silk.NET keyboard / mouse events to ImGui's IO. Replaces what
+/// you'd get from the stock GLFW or SDL backends in a non-Silk.NET host.
+///
+///
+/// Event-driven (we subscribe to Silk.NET events); does not poll. Each
+/// handler writes directly to ImGui.GetIO() via the AddXxx
+/// family of calls. Frame-start book-keeping (display size, delta time,
+/// active modifier latch) happens in .
+///
+///
+///
+/// Call at app shutdown to unsubscribe from Silk.NET
+/// events.
+///
+///
+public sealed class SilkInputBridge : IDisposable
+{
+ private readonly IInputContext _input;
+ private readonly IKeyboard? _keyboard;
+ private readonly IMouse? _mouse;
+
+ public SilkInputBridge(IInputContext input)
+ {
+ _input = input ?? throw new ArgumentNullException(nameof(input));
+
+ _keyboard = input.Keyboards.Count > 0 ? input.Keyboards[0] : null;
+ _mouse = input.Mice.Count > 0 ? input.Mice[0] : null;
+
+ if (_keyboard is not null)
+ {
+ _keyboard.KeyDown += OnKeyDown;
+ _keyboard.KeyUp += OnKeyUp;
+ _keyboard.KeyChar += OnKeyChar;
+ }
+
+ if (_mouse is not null)
+ {
+ _mouse.MouseMove += OnMouseMove;
+ _mouse.MouseDown += OnMouseDown;
+ _mouse.MouseUp += OnMouseUp;
+ _mouse.Scroll += OnScroll;
+ }
+ }
+
+ ///
+ /// Per-frame bookkeeping. Call right before ImGui.NewFrame().
+ /// Sets display size (in logical pixels) and delta-time on ImGui's IO.
+ ///
+ public void BeginFrame(Vector2 displaySize, float deltaSeconds)
+ {
+ var io = Hexa.NET.ImGui.ImGui.GetIO();
+ io.DisplaySize = displaySize;
+ io.DeltaTime = deltaSeconds > 0f ? deltaSeconds : 1f / 60f;
+ }
+
+ // ─── event handlers ──────────────────────────────────────────────
+
+ private void OnKeyDown(IKeyboard kb, Key key, int scancode) => AddKey(key, down: true);
+ private void OnKeyUp (IKeyboard kb, Key key, int scancode) => AddKey(key, down: false);
+
+ private void OnKeyChar(IKeyboard kb, char c)
+ {
+ // Feeds typed text into any focused ImGui TextField. Safe to call
+ // even when no TextField has focus — ImGui buffers the character
+ // and discards it if nothing claims it.
+ Hexa.NET.ImGui.ImGui.GetIO().AddInputCharacter(c);
+ }
+
+ private void OnMouseMove(IMouse m, Vector2 pos)
+ {
+ Hexa.NET.ImGui.ImGui.GetIO().AddMousePosEvent(pos.X, pos.Y);
+ }
+
+ private void OnMouseDown(IMouse m, MouseButton button) => AddMouseButton(button, down: true);
+ private void OnMouseUp (IMouse m, MouseButton button) => AddMouseButton(button, down: false);
+
+ private void OnScroll(IMouse m, ScrollWheel wheel)
+ {
+ Hexa.NET.ImGui.ImGui.GetIO().AddMouseWheelEvent(wheel.X, wheel.Y);
+ }
+
+ // ─── helpers ─────────────────────────────────────────────────────
+
+ private static void AddKey(Key key, bool down)
+ {
+ // Update modifier latches first (ImGui reads these when any AddKeyEvent fires).
+ var io = Hexa.NET.ImGui.ImGui.GetIO();
+ if (key is Key.ControlLeft or Key.ControlRight) io.AddKeyEvent(ImGuiKey.ModCtrl, down);
+ if (key is Key.ShiftLeft or Key.ShiftRight) io.AddKeyEvent(ImGuiKey.ModShift, down);
+ if (key is Key.AltLeft or Key.AltRight) io.AddKeyEvent(ImGuiKey.ModAlt, down);
+ if (key is Key.SuperLeft or Key.SuperRight) io.AddKeyEvent(ImGuiKey.ModSuper, down);
+
+ if (KeyMap.TryGetValue(key, out var imguiKey))
+ io.AddKeyEvent(imguiKey, down);
+ // Unmapped keys are silently ignored — fine for D.2a; panels that
+ // need exotic keys can extend the map.
+ }
+
+ private static void AddMouseButton(MouseButton button, bool down)
+ {
+ int idx = button switch
+ {
+ MouseButton.Left => 0,
+ MouseButton.Right => 1,
+ MouseButton.Middle => 2,
+ _ => -1,
+ };
+ if (idx < 0) return;
+ Hexa.NET.ImGui.ImGui.GetIO().AddMouseButtonEvent(idx, down);
+ }
+
+ ///
+ /// Silk.NET → ImGui key map. Covers text-input + navigation keys +
+ /// WASD + function keys. Unlisted keys fall through to no-op.
+ ///
+ private static readonly Dictionary KeyMap = new()
+ {
+ // Navigation + control
+ [Key.Tab] = ImGuiKey.Tab,
+ [Key.Left] = ImGuiKey.LeftArrow,
+ [Key.Right] = ImGuiKey.RightArrow,
+ [Key.Up] = ImGuiKey.UpArrow,
+ [Key.Down] = ImGuiKey.DownArrow,
+ [Key.PageUp] = ImGuiKey.PageUp,
+ [Key.PageDown] = ImGuiKey.PageDown,
+ [Key.Home] = ImGuiKey.Home,
+ [Key.End] = ImGuiKey.End,
+ [Key.Insert] = ImGuiKey.Insert,
+ [Key.Delete] = ImGuiKey.Delete,
+ [Key.Backspace] = ImGuiKey.Backspace,
+ [Key.Space] = ImGuiKey.Space,
+ [Key.Enter] = ImGuiKey.Enter,
+ [Key.Escape] = ImGuiKey.Escape,
+
+ // Modifiers (also add via the mod-flag path, but these let ImGui
+ // see them as named keys too).
+ [Key.ControlLeft] = ImGuiKey.LeftCtrl,
+ [Key.ControlRight] = ImGuiKey.RightCtrl,
+ [Key.ShiftLeft] = ImGuiKey.LeftShift,
+ [Key.ShiftRight] = ImGuiKey.RightShift,
+ [Key.AltLeft] = ImGuiKey.LeftAlt,
+ [Key.AltRight] = ImGuiKey.RightAlt,
+
+ // Letters (Silk.NET.Key.A..Z map 1:1 to ImGuiKey.A..Z).
+ [Key.A] = ImGuiKey.A, [Key.B] = ImGuiKey.B, [Key.C] = ImGuiKey.C, [Key.D] = ImGuiKey.D,
+ [Key.E] = ImGuiKey.E, [Key.F] = ImGuiKey.F, [Key.G] = ImGuiKey.G, [Key.H] = ImGuiKey.H,
+ [Key.I] = ImGuiKey.I, [Key.J] = ImGuiKey.J, [Key.K] = ImGuiKey.K, [Key.L] = ImGuiKey.L,
+ [Key.M] = ImGuiKey.M, [Key.N] = ImGuiKey.N, [Key.O] = ImGuiKey.O, [Key.P] = ImGuiKey.P,
+ [Key.Q] = ImGuiKey.Q, [Key.R] = ImGuiKey.R, [Key.S] = ImGuiKey.S, [Key.T] = ImGuiKey.T,
+ [Key.U] = ImGuiKey.U, [Key.V] = ImGuiKey.V, [Key.W] = ImGuiKey.W, [Key.X] = ImGuiKey.X,
+ [Key.Y] = ImGuiKey.Y, [Key.Z] = ImGuiKey.Z,
+
+ // Digit row
+ [Key.Number0] = ImGuiKey.Key0, [Key.Number1] = ImGuiKey.Key1, [Key.Number2] = ImGuiKey.Key2,
+ [Key.Number3] = ImGuiKey.Key3, [Key.Number4] = ImGuiKey.Key4, [Key.Number5] = ImGuiKey.Key5,
+ [Key.Number6] = ImGuiKey.Key6, [Key.Number7] = ImGuiKey.Key7, [Key.Number8] = ImGuiKey.Key8,
+ [Key.Number9] = ImGuiKey.Key9,
+
+ // Function keys
+ [Key.F1] = ImGuiKey.F1, [Key.F2] = ImGuiKey.F2, [Key.F3] = ImGuiKey.F3, [Key.F4] = ImGuiKey.F4,
+ [Key.F5] = ImGuiKey.F5, [Key.F6] = ImGuiKey.F6, [Key.F7] = ImGuiKey.F7, [Key.F8] = ImGuiKey.F8,
+ [Key.F9] = ImGuiKey.F9, [Key.F10] = ImGuiKey.F10, [Key.F11] = ImGuiKey.F11, [Key.F12] = ImGuiKey.F12,
+ };
+
+ public void Dispose()
+ {
+ if (_keyboard is not null)
+ {
+ _keyboard.KeyDown -= OnKeyDown;
+ _keyboard.KeyUp -= OnKeyUp;
+ _keyboard.KeyChar -= OnKeyChar;
+ }
+ if (_mouse is not null)
+ {
+ _mouse.MouseMove -= OnMouseMove;
+ _mouse.MouseDown -= OnMouseDown;
+ _mouse.MouseUp -= OnMouseUp;
+ _mouse.Scroll -= OnScroll;
+ }
+ }
+}