feat(ui): AcDream.UI.Abstractions layer — IPanel / IPanelRenderer / VitalsVM

Adds the backend-agnostic UI contract layer called for by the 2026-04-24
staged UI strategy (docs/plans/2026-04-24-ui-framework.md). This is the
stable layer both the Phase D.2a Hexa.NET.ImGui backend and the later
D.2b custom retail-look backend implement.

New module `src/AcDream.UI.Abstractions/`:

  * IPanel        — a drawable panel (id/title/visible/Render)
  * IPanelHost    — owns the panel list, drives per-frame dispatch
  * IPanelRenderer — drawing primitives (Begin/End/Text/SameLine/
                    Separator/ProgressBar). Kept small + retail-friendly
                    on purpose — if a widget can't be expressed with
                    dat-sourced sprites+fonts later, don't add it here.
  * ICommandBus   — user-intent publisher; NullCommandBus is D.2a default
  * PanelContext  — per-frame record struct (DeltaSeconds + Commands)
  * Panels/Vitals/
      VitalsVM   — reads CombatState.GetHealthPercent for the local
                   player. Stamina/Mana return null in D.2a; they await
                   a LocalPlayerState cache of PlayerDescription (0x0013)
                   which is filed as a follow-up issue.
      VitalsPanel — first real panel. HP bar always drawn; Stam/Mana
                    appear automatically when the VM returns non-null.

Invariant documented in IPanel's XML doc: no `using Hexa.NET.ImGui` in
panel files, ever. If a widget needs something IPanelRenderer can't
express, the interface grows; panels never reach through.

References AcDream.Core for CombatState. Zero runtime/UI dependencies
— this project compiles headless, perfect for unit testing the
ViewModels.

No visible change yet. Next commits: (2) tests, (3) ImGui backend,
(4) GameWindow hookup + visible panel behind ACDREAM_DEVTOOLS=1.
This commit is contained in:
Erik 2026-04-25 00:24:11 +02:00
parent b9455259f0
commit 8c64ad2eeb
10 changed files with 333 additions and 0 deletions

View file

@ -0,0 +1,43 @@
namespace AcDream.UI.Abstractions;
/// <summary>
/// Drawing primitives exposed to panels. The <b>only</b> API panels use to
/// emit pixels. The ImGui backend maps these straight onto ImGui calls; the
/// later custom retail-look backend will map the same primitives onto its
/// own retained-mode toolkit using retail dat-sourced fonts / sprites.
///
/// <para>
/// Keep this interface small and retail-friendly. If a widget requires a
/// feature the custom backend couldn't express with dat assets, don't add
/// it — find a different widget shape that both backends can satisfy.
/// </para>
/// </summary>
public interface IPanelRenderer
{
/// <summary>
/// Begin a top-level window. Matches retail's root <c>UiPanel</c> +
/// ImGui's <c>Begin</c>. Returns <c>false</c> if the window is collapsed
/// — the caller must still call <see cref="End"/> to balance.
/// </summary>
bool Begin(string title);
/// <summary>Close the most recent <see cref="Begin"/>.</summary>
void End();
/// <summary>Draw a single line of text. No formatting / markdown.</summary>
void Text(string text);
/// <summary>Keep the next widget on the same line as the previous one.</summary>
void SameLine();
/// <summary>Horizontal rule separator.</summary>
void Separator();
/// <summary>
/// A filled progress bar.
/// <paramref name="fraction"/> is clamped by the backend to [0, 1].
/// <paramref name="width"/> is the pixel width of the full bar.
/// <paramref name="overlay"/> is optional text (e.g. <c>"54%"</c>) rendered on top.
/// </summary>
void ProgressBar(float fraction, float width, string? overlay = null);
}