feat(ui): AcDream.UI.ImGui backend — Hexa.NET.ImGui + Silk.NET input bridge
Second piece of Phase D.2a: the ImGui-specific backend that implements
AcDream.UI.Abstractions' IPanelRenderer / IPanelHost. No GameWindow
hookup yet — compiles standalone for clean review before integration.
Packages:
* Hexa.NET.ImGui 2.2.9 (auto-generated from cimgui 1.92.2b)
* Hexa.NET.ImGui.Backends 1.0.18 (consolidated — OpenGL3 is here)
* Silk.NET.Input 2.23.0 + Silk.NET.OpenGL 2.23.0 (matches AcDream.App)
Files:
ImGuiBootstrapper.cs
One-shot static Initialize(glslVersion) / Shutdown() pair. Creates
the ImGui context, applies dark style, enables NavEnableKeyboard,
and boots ImGuiImplOpenGL3. Re-init is a no-op.
SilkInputBridge.cs
Event-driven Silk.NET -> ImGui IO bridge. Subscribes on construction;
Dispose() unsubscribes. Covers:
- KeyDown/Up -> ImGui.AddKeyEvent with modifier latching
(Ctrl/Shift/Alt/Super routed via both ModXxx flags AND named
key events so both IsKeyPressed checks and ImGui shortcut
matching work)
- KeyChar -> AddInputCharacter for text fields
- MouseMove -> AddMousePosEvent
- MouseDown/Up -> AddMouseButtonEvent (L=0, R=1, M=2)
- Scroll -> AddMouseWheelEvent (both axes)
Silk.NET.Input.Key -> ImGuiKey map covers WASD, arrows, modifiers,
letters, digits, function keys. Unmapped keys silently ignored.
BeginFrame(displaySize, dt) sets IO.DisplaySize + IO.DeltaTime.
ImGuiPanelRenderer.cs
IPanelRenderer impl — one-line wrappers on ImGui.Begin/End,
TextUnformatted, SameLine, Separator, ProgressBar. The ONLY place
Hexa.NET.ImGui types appear outside bootstrap/input plumbing. Panels
still never import ImGui.
ImGuiPanelHost.cs
IPanelHost impl. Dictionary keyed by IPanel.Id for idempotent
Register. RenderAll iterates visible panels and calls their Render.
Does NOT call ImGui.NewFrame / ImGui.Render — ownership belongs to
the caller (GameWindow) so GL state is explicit. Diagnostic `Count`
property.
No behavior change yet; next commit wires this into GameWindow behind
ACDREAM_DEVTOOLS=1 and ships the first visible VitalsPanel.
This commit is contained in:
parent
fc03fa377b
commit
a7dbce3474
6 changed files with 361 additions and 0 deletions
|
|
@ -7,6 +7,7 @@
|
|||
<Project Path="src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj" />
|
||||
<Project Path="src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj" />
|
||||
<Project Path="src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj" />
|
||||
<Project Path="src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tools/">
|
||||
<Project Path="tools/RetailTimeProbe/RetailTimeProbe.csproj" />
|
||||
|
|
|
|||
23
src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj
Normal file
23
src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Hexa.NET.ImGui backend — chosen over ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui
|
||||
for AOT-ready native-lib bundling and closer tracking of upstream cimgui.
|
||||
See docs/plans/2026-04-24-ui-framework.md §"Choice: Hexa.NET.ImGui". -->
|
||||
<PackageReference Include="Hexa.NET.ImGui" Version="2.2.9" />
|
||||
<PackageReference Include="Hexa.NET.ImGui.Backends" Version="1.0.18" />
|
||||
<!-- SilkInputBridge references Silk.NET.Input types directly so we need
|
||||
the package even though AcDream.App transitively has it. -->
|
||||
<PackageReference Include="Silk.NET.Input" Version="2.23.0" />
|
||||
<PackageReference Include="Silk.NET.OpenGL" Version="2.23.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.UI.Abstractions\AcDream.UI.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
62
src/AcDream.UI.ImGui/ImGuiBootstrapper.cs
Normal file
62
src/AcDream.UI.ImGui/ImGuiBootstrapper.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using Hexa.NET.ImGui;
|
||||
using Hexa.NET.ImGui.Backends.OpenGL3;
|
||||
|
||||
namespace AcDream.UI.ImGui;
|
||||
|
||||
/// <summary>
|
||||
/// One-shot ImGui setup / teardown for the devtools overlay. Called from
|
||||
/// <c>GameWindow</c> when <c>ACDREAM_DEVTOOLS=1</c>. Hides the cimgui
|
||||
/// context + OpenGL3 renderer-impl lifecycles behind two static methods
|
||||
/// so the calling code stays clean.
|
||||
///
|
||||
/// <para>
|
||||
/// Intentionally <b>not</b> an <c>IDisposable</c> singleton — the host
|
||||
/// window owns the one call to <see cref="Shutdown"/> at application
|
||||
/// exit. Re-initialisation mid-session is not supported.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ImGuiBootstrapper
|
||||
{
|
||||
private static bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="glslVersion">
|
||||
/// GLSL version directive for the ImGui-internal shader.
|
||||
/// <c>"#version 330"</c> matches acdream's existing shaders and is
|
||||
/// the safest default for the OpenGL 4.3 core profile we ship.
|
||||
/// </param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Tear down the OpenGL3 renderer + destroy the ImGui context.</summary>
|
||||
public static void Shutdown()
|
||||
{
|
||||
if (!_initialized) return;
|
||||
|
||||
ImGuiImplOpenGL3.Shutdown();
|
||||
Hexa.NET.ImGui.ImGui.DestroyContext();
|
||||
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
/// <summary>True after <see cref="Initialize"/> has run successfully.</summary>
|
||||
public static bool IsInitialized => _initialized;
|
||||
}
|
||||
46
src/AcDream.UI.ImGui/ImGuiPanelHost.cs
Normal file
46
src/AcDream.UI.ImGui/ImGuiPanelHost.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using AcDream.UI.Abstractions;
|
||||
|
||||
namespace AcDream.UI.ImGui;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IPanelHost"/> implementation for the ImGui backend. Owns the
|
||||
/// registered panel set; iterates + draws every frame when the caller is
|
||||
/// inside an ImGui frame (between <c>ImGui.NewFrame</c> and
|
||||
/// <c>ImGui.Render</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// <b>This class does not call <c>ImGui.NewFrame</c> / <c>ImGui.Render</c>
|
||||
/// itself.</b> Those belong to the caller (GameWindow) so GL-state
|
||||
/// ownership is explicit and the render-loop integration point is obvious.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ImGuiPanelHost : IPanelHost
|
||||
{
|
||||
private readonly Dictionary<string, IPanel> _panels = new();
|
||||
private readonly ImGuiPanelRenderer _renderer = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(IPanel panel)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(panel);
|
||||
_panels[panel.Id] = panel; // idempotent by Id
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Unregister(string panelId) => _panels.Remove(panelId);
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Current registered count (for diagnostics).</summary>
|
||||
public int Count => _panels.Count;
|
||||
}
|
||||
41
src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
Normal file
41
src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
using System.Numerics;
|
||||
using AcDream.UI.Abstractions;
|
||||
|
||||
namespace AcDream.UI.ImGui;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IPanelRenderer"/> 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.
|
||||
/// </summary>
|
||||
public sealed class ImGuiPanelRenderer : IPanelRenderer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool Begin(string title) => Hexa.NET.ImGui.ImGui.Begin(title);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void End() => Hexa.NET.ImGui.ImGui.End();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Text(string text) => Hexa.NET.ImGui.ImGui.TextUnformatted(text);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SameLine() => Hexa.NET.ImGui.ImGui.SameLine();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Separator() => Hexa.NET.ImGui.ImGui.Separator();
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
188
src/AcDream.UI.ImGui/SilkInputBridge.cs
Normal file
188
src/AcDream.UI.ImGui/SilkInputBridge.cs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
using System.Numerics;
|
||||
using Hexa.NET.ImGui;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace AcDream.UI.ImGui;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
///
|
||||
/// <para>
|
||||
/// Event-driven (we subscribe to Silk.NET events); does not poll. Each
|
||||
/// handler writes directly to <c>ImGui.GetIO()</c> via the <c>AddXxx</c>
|
||||
/// family of calls. Frame-start book-keeping (display size, delta time,
|
||||
/// active modifier latch) happens in <see cref="BeginFrame"/>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Call <see cref="Dispose"/> at app shutdown to unsubscribe from Silk.NET
|
||||
/// events.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame bookkeeping. Call right before <c>ImGui.NewFrame()</c>.
|
||||
/// Sets display size (in logical pixels) and delta-time on ImGui's IO.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Silk.NET → ImGui key map. Covers text-input + navigation keys +
|
||||
/// WASD + function keys. Unlisted keys fall through to no-op.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Key, ImGuiKey> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue