diff --git a/.gitignore b/.gitignore index 1731f2e..904fdf9 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ references/ # Claude Code session state .claude/ launch.log +launch-*.log + +# ImGui auto-saved window/docking state (per-user, not source) +imgui.ini diff --git a/CLAUDE.md b/CLAUDE.md index 84c1d80..96449ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,13 +25,14 @@ The codebase is organized by layer (see architecture doc). Current phase state lives in memory (`memory/project_*.md`), plans in `docs/plans/`, research in `docs/research/`. -**UI strategy:** three-layer split — swappable backend (Hexa.NET.ImGui for -Phase D.2a short-term, custom retail-look toolkit for D.2b later) / -stable `AcDream.UI.Abstractions` layer (ViewModels + Commands + `IPanel` -/ `IPanelRenderer`) / unchanged game state. **All plugin-facing UI -targets `AcDream.UI.Abstractions` — never import a backend namespace -from a panel.** Full design: `docs/plans/2026-04-24-ui-framework.md`. -Memory crib: `memory/project_ui_architecture.md`. +**UI strategy:** three-layer split — swappable backend (ImGui.NET + +`Silk.NET.OpenGL.Extensions.ImGui` for Phase D.2a short-term, custom +retail-look toolkit for D.2b later) / stable `AcDream.UI.Abstractions` +layer (ViewModels + Commands + `IPanel` / `IPanelRenderer`) / unchanged +game state. **All plugin-facing UI targets `AcDream.UI.Abstractions` — +never import a backend namespace from a panel.** Full design: +`docs/plans/2026-04-24-ui-framework.md`. Memory crib: +`memory/project_ui_architecture.md`. ## How to operate diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md index e5f7f85..80536a1 100644 --- a/docs/architecture/acdream-architecture.md +++ b/docs/architecture/acdream-architecture.md @@ -74,7 +74,8 @@ designed 2026-04-24. Full design: `docs/plans/2026-04-24-ui-framework.md`. ``` ┌─────────────────────────────────────────────────────────────┐ │ UI BACKEND (swappable) │ -│ Hexa.NET.ImGui (Phase D.2a, short-term) │ +│ ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui │ +│ (Phase D.2a, short-term) │ │ or custom retail-look toolkit (Phase D.2b, later) │ ├─────────────────────────────────────────────────────────────┤ │ AcDream.UI.Abstractions (stable contract) │ diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 1559590..aabe568 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -46,6 +46,7 @@ | G.1+ | Full sky visuals + weather + dynamic-light shader — SkyDescLoader parses Region 0x13000000 dat keyframes with retail fog fields (start/end/mode); WeatherSystem picks Clear/Overcast/Rain/Snow/Storm deterministically per in-game day with 10s fade; SkyRenderer draws far-plane-1e6 celestial meshes with UV scroll; SceneLightingUbo binds at std140 location=1 with 8 Light slots + fog + lightning flash; terrain.vert + mesh.frag + mesh_instanced.frag + sky.frag all consume the shared UBO; LightingHookSink auto-registers Setup.Lights per entity + flips IsLit on SetLightHook; ParticleRenderer renders rain/snow billboards; F7 cycles day time override, F10 cycles weather; WorldSession surfaces server time via ServerTimeUpdated (ConnectRequest + TimeSync flag) | Tests ✓ | | H.1 | Chat wire layer — Talk (0x0015) / Tell (0x005D) / ChatChannel (0x0147) outbound, HearSpeech (0x02BB local + 0x02BC ranged) inbound, ChatLog ring buffer with adapters for every chat source | Tests ✓ | | Glue | GameEventWiring.WireAll — single-call registration mapping parsed GameEvents → Core state classes (ChatLog, CombatState, Spellbook, ItemRepository); GameWindow exposes state classes + wires them to live session | Tests ✓ | +| D.2a | UI scaffold — `AcDream.UI.Abstractions` stable contract (`IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + `VitalsVM` / `VitalsPanel`); `AcDream.UI.ImGui` backend on ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` (pivoted from Hexa.NET.ImGui on 2026-04-25 — Hexa's native OpenGL3 backend resolves GL via GLFW/SDL and crashed 0xC0000005 without them); VitalsPanel wired into GameWindow behind `ACDREAM_DEVTOOLS=1` with `ImGui.WantCaptureKeyboard` WASD suppression. 11 new tests. | Live ✓ | Plus polish that doesn't get its own phase number: - FlyCamera default speed lowered + Shift-to-boost @@ -126,9 +127,11 @@ Plus polish that doesn't get its own phase number: > [`docs/plans/2026-04-24-ui-framework.md`](2026-04-24-ui-framework.md) > for the full design. Short version: > -> 1. **D.2a — Hexa.NET.ImGui as the short-term backend.** Wire up in days, +> 1. **D.2a — ImGui as the short-term backend.** Wire up in days, > iterate game logic (chat-send, inventory actions, vitals HUD reading -> real state) in weeks. Looks like a debugger; that's fine. +> real state) in weeks. Looks like a debugger; that's fine. *(Shipped +> 2026-04-25 on ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` after a +> day-one pivot away from Hexa.NET.ImGui; see shipped table.)* > 2. **Stable `AcDream.UI.Abstractions` layer** — ViewModels + Commands + > `IPanel` / `IPanelRenderer` interfaces. Backend-agnostic. Plugin API > publishes against this layer and never sees ImGui. @@ -142,7 +145,7 @@ Plus polish that doesn't get its own phase number: **Sub-pieces:** - **D.1 — 2D ortho overlay + font rendering.** ✅ SHIPPED 2026-04-17 as the dev-facing debug overlay (StbTrueTypeSharp system-font atlas + `TextRenderer` + `DebugOverlay`). -- **D.2a — Hexa.NET.ImGui scaffold + `AcDream.UI.Abstractions` layer.** NEW pre-piece introduced 2026-04-24. Wires Hexa.NET.ImGui as the short-term backend behind `ACDREAM_DEVTOOLS=1`. Defines `IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + the first ViewModels (`VitalsVM` etc.) in the new `AcDream.UI.Abstractions` project. First real panel: `VitalsPanel` reading HP/stam/mana from `IGameState`. This is what gets game-logic iteration moving; looks like a debugger, acceptable. +- **✓ SHIPPED — D.2a — ImGui scaffold + `AcDream.UI.Abstractions` layer.** Shipped 2026-04-25. Wires ImGui as the short-term backend behind `ACDREAM_DEVTOOLS=1`. Defines `IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + the first ViewModel (`VitalsVM`) in the new `AcDream.UI.Abstractions` project. First real panel: `VitalsPanel` reading HP from `CombatState.GetHealthPercent`. **Backend pivoted Hexa.NET.ImGui → ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` during integration** — Hexa's native OpenGL3 backend does its own GL function resolution via GLFW/SDL and crashed with `0xC0000005` in `ImGuiImplOpenGL3.InitNative` against Silk.NET (no GLFW/SDL present). The Silk.NET extension is purpose-built for this scenario and is the `ImGui.NET` mitigation path that `docs/plans/2026-04-24-ui-framework.md` already called out as a "one-morning operation". Stam/Mana return `float?` null in D.2a because absolute values need `LocalPlayerState` + `PlayerDescription (0x0013)` parsing (filed post-D.2a). 11 new `AcDream.UI.Abstractions.Tests` green. - **D.2b — Custom retail-look backend.** Implements the same `IPanel` / `IPanelRenderer` contracts using a custom retained-mode toolkit sourced from retail dat assets. Requires D.2a shipped. Panels get reskinned one at a time; ImGui stays as the `ACDREAM_DEVTOOLS=1` overlay forever. The original 2026-04-17 scaffold research (`UiRoot` / `UiElement` / `UiPanel` / `UiHost` + retail event codes + focus / drag-drop state machine + `WorldMouseFallThrough`) is the implementation foundation here — see `docs/research/retail-ui/`. - **D.3 — AcFont from portal.dat.** Replace stb_truetype system font with retail `Font` DBObjs (`0x40000000..0x40000FFF`) baked from `RenderSurface` source sheets — see research slice 03 §4. Preserves retail visual identity. **(D.2b dependency — needs the custom renderer.)** - **D.4 — Dat sprites + 9-slice panel backgrounds.** Load `RenderSurface` (`0x06xxxxxx`) as GL textures; add `DrawSprite` to `UiRenderContext`. Enables retail panel art. **(D.2b dependency.)** diff --git a/docs/plans/2026-04-24-ui-framework.md b/docs/plans/2026-04-24-ui-framework.md index 85d8a41..e6fce26 100644 --- a/docs/plans/2026-04-24-ui-framework.md +++ b/docs/plans/2026-04-24-ui-framework.md @@ -1,13 +1,51 @@ # UI framework plan -**Date:** 2026-04-24 -**Status:** design — not yet implemented +**Date:** 2026-04-24 (design), shipped 2026-04-25 +**Status:** **Phase D.2a shipped** — `AcDream.UI.Abstractions` + ImGui backend ++ `VitalsPanel` gated on `ACDREAM_DEVTOOLS=1`. Backend pivoted from +`Hexa.NET.ImGui` to `ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui` during +first-light integration — see the pivot note below. Phase D.2b (custom +retail-look backend) remains design-only. **Owner:** lead engineer (erik) + Claude Captures the UI strategy agreed via discussion on 2026-04-24. Documents the choices AND the alternatives considered so future sessions can re-evaluate with the same context. +## 2026-04-25 pivot: Hexa.NET.ImGui → ImGui.NET + +The original choice (documented below) was `Hexa.NET.ImGui` + +`Hexa.NET.ImGui.Backends.OpenGL3`. It did not survive first-light +integration: + +- First launch with Hexa's backend crashed with `0xC0000005` inside + `Hexa.NET.ImGui.Backends.OpenGL3.ImGuiImplOpenGL3.InitNative`. +- Root cause: Hexa's native OpenGL3 backend does its own GL function + resolution, looking up symbols via GLFW or SDL. Silk.NET uses neither, + so the resolved function pointers were null and the native code + dereferenced them on init. +- Hexa's Silk.NET examples rely on GLFW being co-loaded (its default + on Hexa's own scenes) — not applicable here. + +**Mitigation path** was already written into this doc (§"What we give +up": *"switching to ImGui.NET later is a one-morning operation if Hexa +misbehaves"*) and taken: + +- Packages swapped → `ImGui.NET 1.91.6.1` + `Silk.NET.OpenGL.Extensions.ImGui 2.23.0`. +- `Silk.NET.OpenGL.Extensions.ImGui.ImGuiController` handles the whole + integration (GL backend init against the Silk.NET GL binding, + keyboard + mouse IO event subscription). No hand-written input + bridge needed. +- `ImGuiBootstrapper` is a ~10-line `IDisposable` wrapping the + `ImGuiController` instance. `ImGuiPanelRenderer` wraps `ImGuiNET.ImGui.*`. +- Boundary discipline preserved — panels never import `ImGuiNET` + directly; they only use `IPanelRenderer`. The backend swap is + invisible above the abstraction layer, as designed. + +Sections below from §"Choice: Hexa.NET.ImGui" onward are kept as the +historical design reasoning. They remain useful if we ever re-evaluate +native AOT / upstream-tracking tradeoffs. + ## Goal acdream needs a playable game UI: chat, vitals HUD, inventory, character diff --git a/docs/research/retail-ui/00-master-synthesis.md b/docs/research/retail-ui/00-master-synthesis.md index 7882a9c..0dbbf77 100644 --- a/docs/research/retail-ui/00-master-synthesis.md +++ b/docs/research/retail-ui/00-master-synthesis.md @@ -1,10 +1,12 @@ # Retail AC Client GUI — Master Synthesis -> **Scope note (2026-04-24):** This document describes retail's Keystone -> UI toolkit — it is the research foundation for **Phase D.2b (custom -> retail-look backend)**, not **Phase D.2a (Hexa.NET.ImGui scaffold)**. -> When reading this for implementation guidance, assume D.2a has shipped -> a working `AcDream.UI.Abstractions` layer (`IPanel`, `IPanelRenderer`, +> **Scope note (2026-04-24, updated 2026-04-25):** This document +> describes retail's Keystone UI toolkit — it is the research foundation +> for **Phase D.2b (custom retail-look backend)**, not Phase D.2a +> (shipped ImGui scaffold, `AcDream.UI.Abstractions` + ImGui.NET + +> `Silk.NET.OpenGL.Extensions.ImGui` + `VitalsPanel`). When reading this +> for implementation guidance, assume D.2a has shipped a working +> `AcDream.UI.Abstractions` layer (`IPanel`, `IPanelRenderer`, > ViewModels, Commands) and you are building the custom retained-mode > toolkit that implements the same contracts using dat-sourced fonts / > sprites / cursors. See `docs/plans/2026-04-24-ui-framework.md` for the diff --git a/memory/project_ui_architecture.md b/memory/project_ui_architecture.md index 2d6240d..bf5e4ad 100644 --- a/memory/project_ui_architecture.md +++ b/memory/project_ui_architecture.md @@ -15,10 +15,14 @@ └─────────────────────────────────────────┘ ``` -- **UI backend** (bottom swap axis): `Hexa.NET.ImGui` for Phase D.2a +- **UI backend** (bottom swap axis): **`ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui`** for Phase D.2a (short-term, debugger-look, validates game logic fast). Custom retail-look toolkit for Phase D.2b (long-term, uses dat assets). ImGui stays **forever** as the `ACDREAM_DEVTOOLS=1` overlay. + (Pivoted from `Hexa.NET.ImGui` on 2026-04-25 — Hexa's native OpenGL3 + backend resolves GL via GLFW/SDL internally and crashed `0xC0000005` + against Silk.NET; the Silk.NET extension is purpose-built for this + stack.) - **ViewModels + Commands** (the stable contract): per-panel data records (`VitalsVM`, `InventoryVM`, `ChatVM`, …) and action records (`UseItemCmd`, `SendChatCmd`, `CastSpellCmd`, …). Lives in @@ -32,16 +36,19 @@ - `src/AcDream.UI.Abstractions/` — `IPanel`, `IPanelHost`, `IPanelRenderer`, `ICommandBus`, all ViewModels + Commands. Backend- agnostic. -- `src/AcDream.UI.ImGui/` — Hexa.NET.ImGui-based implementation of - `IPanelRenderer` + ImGui bootstrap. Phase D.2a. +- `src/AcDream.UI.ImGui/` — `ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui` + implementation of `IPanelRenderer` + ImGui bootstrap. Phase D.2a. + `ImGuiController` (from the Silk.NET extension) handles GL backend + + input event subscription; `ImGuiBootstrapper` is a thin IDisposable + wrapper; `ImGuiPanelRenderer` wraps the widgets `IPanelRenderer` needs. - `src/AcDream.UI.Retail/` (later) — custom retained-mode toolkit using dat assets, same `IPanelRenderer` contract. Phase D.2b. ## Hard rules 1. **No panel references a backend namespace.** If a panel imports - `Hexa.NET.ImGui` or a custom-toolkit widget class directly, it's a - bug. + `ImGuiNET` / `Silk.NET.OpenGL.Extensions.ImGui` or a custom-toolkit + widget class directly, it's a bug — extend `IPanelRenderer` instead. 2. **Plugin API targets the abstraction layer only.** Plugins define `IPanel` instances; they never see which backend draws them. 3. **Features that only ImGui can express → not in `IPanelRenderer`.** diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj index 277ae99..6fb3af0 100644 --- a/src/AcDream.App/AcDream.App.csproj +++ b/src/AcDream.App/AcDream.App.csproj @@ -24,6 +24,8 @@ + + diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index cbd4b27..ab4479f 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -281,6 +281,14 @@ public sealed class GameWindow : IDisposable public readonly AcDream.Core.Spells.Spellbook SpellBook = new(); public readonly AcDream.Core.Items.ItemRepository Items = new(); + // Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1. + // See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy. + private AcDream.UI.ImGui.ImGuiBootstrapper? _imguiBootstrap; + private AcDream.UI.ImGui.ImGuiPanelHost? _panelHost; + private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm; + private static readonly bool DevToolsEnabled = + Environment.GetEnvironmentVariable("ACDREAM_DEVTOOLS") == "1"; + // Phase G.1-G.2 world lighting/time state. public readonly AcDream.Core.World.WorldTimeService WorldTime = new AcDream.Core.World.WorldTimeService( @@ -870,6 +878,35 @@ public sealed class GameWindow : IDisposable } } + // Phase D.2a — ImGui devtools overlay. Zero cost when the env var + // isn't set: no context creation, no per-frame branches hit. + // See docs/plans/2026-04-24-ui-framework.md + memory/project_ui_architecture.md. + if (DevToolsEnabled) + { + try + { + _imguiBootstrap = new AcDream.UI.ImGui.ImGuiBootstrapper(_gl!, _window!, _input!); + _panelHost = new AcDream.UI.ImGui.ImGuiPanelHost(); + + // VitalsVM: GUID=0 at construction; set later at EnterWorld + // (see the _playerServerGuid assignment path). Pre-login the + // HP bar just reads 1.0 (safe default) — harmless. + _vitalsVm = new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat); + _panelHost.Register( + new AcDream.UI.Abstractions.Panels.Vitals.VitalsPanel(_vitalsVm)); + + Console.WriteLine("devtools: ImGui panel host ready (VitalsPanel registered)"); + } + catch (Exception ex) + { + Console.WriteLine($"devtools: ImGui init failed: {ex.Message} — devtools disabled"); + _imguiBootstrap?.Dispose(); + _imguiBootstrap = null; + _panelHost = null; + _vitalsVm = null; + } + } + uint centerLandblockId = 0xA9B4FFFFu; Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}"); @@ -1149,6 +1186,7 @@ public sealed class GameWindow : IDisposable var chosen = _liveSession.Characters.Characters[0]; _playerServerGuid = chosen.Id; // Phase B.2: store for Tab-key player-mode entry + _vitalsVm?.SetLocalPlayerGuid(chosen.Id); // Phase D.2a — devtools HP bar tracks this guid _worldState.MarkPersistent(chosen.Id); // player entity survives landblock unloads Console.WriteLine($"live: entering world as 0x{chosen.Id:X8} {chosen.Name}"); _liveSession.EnterWorld(user, characterIndex: 0); @@ -3502,6 +3540,13 @@ public sealed class GameWindow : IDisposable var kb = _input.Keyboards[0]; + // Phase D.2a — suppress game-side WASD / interaction polling when + // ImGui has keyboard focus (e.g. a text field is active). Without + // this, typing "walk" into a chat field would actually walk. + bool suppressGameInput = + DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard; + if (suppressGameInput) return; + if (_cameraController.IsFlyMode) { _cameraController.Fly.Update( @@ -3709,6 +3754,12 @@ public sealed class GameWindow : IDisposable _gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + // Phase D.2a — begin ImGui frame. Paired with the Render() call + // after the scene draws (below). ImGuiController.Update() + // consumes buffered Silk.NET input events and calls ImGui.NewFrame. + if (DevToolsEnabled && _imguiBootstrap is not null) + _imguiBootstrap.BeginFrame((float)deltaSeconds); + // Phase 6.4: advance per-entity animation playback before drawing // so the renderer always sees the up-to-date per-part transforms. if (_animatedEntities.Count > 0) @@ -4002,6 +4053,20 @@ public sealed class GameWindow : IDisposable } } + // Phase D.2a — end ImGui frame. Runs AFTER all scene + debug draws + // so ImGui composites on top. ImGuiController save/restores the + // GL state it touches (blend, scissor, VAO, shader, texture); any + // state not in its save-list (e.g. GL_FRAMEBUFFER_SRGB, unused + // today) would need manual protection. + if (DevToolsEnabled && _imguiBootstrap is not null && _panelHost is not null) + { + var ctx = new AcDream.UI.Abstractions.PanelContext( + (float)deltaSeconds, + AcDream.UI.Abstractions.NullCommandBus.Instance); + _panelHost.RenderAll(ctx); + _imguiBootstrap.Render(); + } + // Update the window title with performance stats every ~0.5s. _perfAccum += deltaSeconds; _perfFrameCount++; diff --git a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj b/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj index 65853fa..66f5bba 100644 --- a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj +++ b/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj @@ -7,15 +7,20 @@ true - - - - + + + + diff --git a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs b/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs index 70ba689..bbcc3a8 100644 --- a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs +++ b/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs @@ -1,62 +1,64 @@ -using Hexa.NET.ImGui; -using Hexa.NET.ImGui.Backends.OpenGL3; +using Silk.NET.Input; +using Silk.NET.OpenGL; +using Silk.NET.OpenGL.Extensions.ImGui; +using Silk.NET.Windowing; 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. +/// Owns the ImGuiController from Silk.NET.OpenGL.Extensions.ImGui, +/// which handles the whole Silk.NET ↔ ImGui.NET integration: +/// +/// Creates the ImGui context + OpenGL3 backend using Silk.NET's GL binding +/// (no GLFW / SDL dependency — unlike Hexa.NET.ImGui, which assumed one). +/// Subscribes to Silk.NET's window + input events to drive IO. +/// Per frame: Update(dt) calls ImGui.NewFrame(); Render() +/// calls ImGui.Render() + uploads draw data via its OpenGL3 backend. +/// /// /// -/// Intentionally not an IDisposable singleton — the host -/// window owns the one call to at application -/// exit. Re-initialisation mid-session is not supported. +/// Instance-scoped rather than static so GL-context lifetime is explicit. +/// GameWindow owns the one instance and disposes on shutdown. +/// +/// +/// +/// History: tried Hexa.NET.ImGui + Hexa.NET.ImGui.Backends.OpenGL3 first +/// per the original plan, but its native OpenGL3 backend resolves GL functions +/// via GLFW / SDL internally and crashed (0xC0000005) in InitNative without +/// one of those present. Pivoted to the official Silk.NET extension on 2026-04-25. /// /// -public static class ImGuiBootstrapper +public sealed class ImGuiBootstrapper : IDisposable { - private static bool _initialized; + private readonly ImGuiController _controller; + + public ImGuiBootstrapper(GL gl, IView window, IInputContext input) + { + ArgumentNullException.ThrowIfNull(gl); + ArgumentNullException.ThrowIfNull(window); + ArgumentNullException.ThrowIfNull(input); + // ImGuiController constructor handles: + // - ImGui.CreateContext() + // - ImGuiOpenGL3 shader + vertex-buffer init (via Silk.NET GL) + // - Keyboard + mouse event subscription (bound to Silk.NET IInputContext) + // - Default style = dark + _controller = new ImGuiController(gl, window, input); + } /// - /// 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. + /// Begin an ImGui frame. Call BEFORE any ImGui.* widget calls. + /// Internally: consumes buffered input events, calls ImGui.NewFrame(). /// - /// - /// 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; + public void BeginFrame(float deltaSeconds) => _controller.Update(deltaSeconds); - Hexa.NET.ImGui.ImGui.CreateContext(); - Hexa.NET.ImGui.ImGui.StyleColorsDark(); + /// + /// Finalise the ImGui frame and draw to the framebuffer. Call AFTER all + /// panel draws, within the same frame as . The + /// OpenGL3 backend save/restores the GL state it touches (shader, VAO, + /// texture, blend, scissor); state not in its save-list (e.g. + /// GL_FRAMEBUFFER_SRGB) is caller's responsibility. + /// + public void Render() => _controller.Render(); - 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; + public void Dispose() => _controller.Dispose(); } diff --git a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs index 17e463d..d1fde2f 100644 --- a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs +++ b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs @@ -1,31 +1,32 @@ using System.Numerics; using AcDream.UI.Abstractions; +using ImGuiNET; 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. +/// ImGui.NET calls. This is the ONLY place where ImGuiNET types appear +/// outside of bootstrap plumbing — panels that need a feature must +/// extend the abstraction here, not by importing ImGuiNET in panel +/// files. /// public sealed class ImGuiPanelRenderer : IPanelRenderer { /// - public bool Begin(string title) => Hexa.NET.ImGui.ImGui.Begin(title); + public bool Begin(string title) => ImGuiNET.ImGui.Begin(title); /// - public void End() => Hexa.NET.ImGui.ImGui.End(); + public void End() => ImGuiNET.ImGui.End(); /// - public void Text(string text) => Hexa.NET.ImGui.ImGui.TextUnformatted(text); + public void Text(string text) => ImGuiNET.ImGui.TextUnformatted(text); /// - public void SameLine() => Hexa.NET.ImGui.ImGui.SameLine(); + public void SameLine() => ImGuiNET.ImGui.SameLine(); /// - public void Separator() => Hexa.NET.ImGui.ImGui.Separator(); + public void Separator() => ImGuiNET.ImGui.Separator(); /// public void ProgressBar(float fraction, float width, string? overlay = null) @@ -36,6 +37,6 @@ public sealed class ImGuiPanelRenderer : IPanelRenderer 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); + ImGuiNET.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty); } } diff --git a/src/AcDream.UI.ImGui/SilkInputBridge.cs b/src/AcDream.UI.ImGui/SilkInputBridge.cs deleted file mode 100644 index 6c47f28..0000000 --- a/src/AcDream.UI.ImGui/SilkInputBridge.cs +++ /dev/null @@ -1,188 +0,0 @@ -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; - } - } -}