acdream/src/AcDream.UI.Abstractions/IPanelRenderer.cs
Erik f42c164b90 feat(ui): #25 Phase K.3 — Settings panel + click-to-rebind + Phase K shipped
Phase K final commit. Settings panel with click-to-rebind UX on top of
the K.1+K.2 input architecture, plus the roadmap / ISSUES / memory
updates that retire Phase K.

InputDispatcher gains BeginCapture / CancelCapture / IsCapturing /
SetBindings — modal capture suppresses normal action firing for the
next chord. Esc cancels (returns sentinel default chord); modifier-only
keys don't complete capture; non-modifier key down with current
modifier mask completes.

IPanelRenderer + ImGuiPanelRenderer + FakePanelRenderer gain
BeginMainMenuBar / EndMainMenuBar / BeginMenu / EndMenu / MenuItem
primitives.

SettingsVM owns a draft copy of KeyBindings with explicit Save /
Cancel / Reset semantics. Click-to-rebind enters dispatcher capture
mode; on chord captured, conflict-detect against draft (excluding the
action being rebound itself); surface a ConflictPrompt when the chord
collides; ResolveConflict(replace=true|false) commits or reverts.
ResetActionToDefault restores a single action to RetailDefaults();
ResetAllToDefaults rebuilds the entire draft. Save invokes the
onSave callback (which writes JSON + swaps the live dispatcher's
bindings).

SettingsPanel renders 8 retail-keymap-categorized CollapsingHeader
sections (Movement, Postures, Camera, Combat, UI panels, Chat,
Hotbar, Emotes). Per action: name + current binding(s) summary +
"Rebind"/"Reset" buttons. Conflict prompt at the top when pending.
Save / Cancel / "Reset all to retail defaults" at the top.

GameWindow registers SettingsPanel + wires F11 →
ToggleOptionsPanel → IsVisible toggle, plus a top-of-frame ImGui
MainMenuBar with View → Settings/Vitals/Chat/Debug entries (calls
ImGui directly — the abstraction methods exist for backend
portability but the host doesn't own a menu-bar surface).

Tests: +37 across InputDispatcherCaptureTests (7),
IPanelRendererMainMenuBarTests (9), SettingsVMTests (13),
SettingsPanelTests (8). Solution total 1220 green.

Roadmap (docs/plans/2026-04-11-roadmap.md) appends Phase K shipped
section after Phase J with K.1a–K.3 commit SHAs. ISSUES.md files
Phase L deferred work as #L.1–#L.8 (hotbar UI, spellbook favorites,
combat-mode dispatch, F-key panels, floating chat windows, UI layout
save/load, joystick bindings, plugin input subscription) and adds
#21–#25 to Recently closed. project_input_pipeline.md updated to
shipped state. CLAUDE.md gets an input-pipeline reference.

Closes Phase K.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:44:56 +02:00

238 lines
9.6 KiB
C#

using System.Numerics;
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);
// -- Phase I.1 widget extensions ----------------------------------
/// <summary>
/// Draw a single line of text in the supplied RGBA color.
/// Used for combat-event color-coding (damage red, heal green) and
/// any other surface where a glance distinguishes severity.
/// </summary>
void TextColored(Vector4 rgba, string text);
/// <summary>
/// Open / close section grouping inside a window. Returns
/// <c>true</c> when the section is currently expanded — only render
/// the section's contents in that branch.
/// </summary>
/// <param name="defaultOpen">If the user has never toggled the
/// header before, start in this state.</param>
bool CollapsingHeader(string label, bool defaultOpen = true);
/// <summary>
/// Push an expandable nested node. Pair every successful
/// <c>true</c> return with a matching <see cref="TreePop"/>;
/// when this returns <c>false</c> do NOT call <see cref="TreePop"/>.
/// </summary>
bool TreeNode(string label);
/// <summary>Pop a previously-pushed <see cref="TreeNode"/>.</summary>
void TreePop();
/// <summary>
/// A boolean toggle. Returns <c>true</c> on the frame the user
/// flipped the box (and updates <paramref name="value"/> in place);
/// returns <c>false</c> the rest of the time.
/// </summary>
bool Checkbox(string label, ref bool value);
/// <summary>
/// A clickable button. Returns <c>true</c> on the single frame the
/// user clicked it; <c>false</c> every other frame.
/// </summary>
bool Button(string label);
/// <summary>
/// A drop-down selector. Returns <c>true</c> on the frame the user
/// changed the selection (and updates <paramref name="selectedIndex"/>
/// to the new value); <c>false</c> otherwise.
/// </summary>
bool Combo(string label, ref int selectedIndex, string[] items);
/// <summary>
/// A horizontal slider clamped to <paramref name="min"/> /
/// <paramref name="max"/>. Returns <c>true</c> on frames where the
/// user dragged the value (and updates <paramref name="value"/>);
/// <c>false</c> when idle.
/// </summary>
bool SliderFloat(string label, ref float value, float min, float max);
/// <summary>
/// Time-series line graph (e.g. fps history). The active window is
/// <paramref name="values"/>[<paramref name="offset"/>..
/// <paramref name="offset"/>+<paramref name="count"/>] interpreted
/// as a ring buffer.
/// </summary>
/// <param name="label">Header text shown above the plot.</param>
/// <param name="values">Sample buffer (kept by the caller).</param>
/// <param name="count">Number of valid samples in the ring.</param>
/// <param name="offset">Index of the oldest sample in
/// <paramref name="values"/>.</param>
/// <param name="overlay">Optional centered overlay text (e.g.
/// <c>"60 fps"</c>).</param>
/// <param name="min">Optional fixed lower bound; null = autoscale.</param>
/// <param name="max">Optional fixed upper bound; null = autoscale.</param>
/// <param name="size">Optional pixel size; null = backend default.</param>
void PlotLines(
string label,
float[] values,
int count,
int offset = 0,
string? overlay = null,
float? min = null,
float? max = null,
Vector2? size = null);
/// <summary>
/// Begin a multi-column table. Pair every call with <see cref="EndTable"/>.
/// Inside the table, advance to each cell with
/// <see cref="TableNextColumn"/> before emitting widgets for it.
/// </summary>
void BeginTable(string id, int columns);
/// <summary>Move to the next cell of the active table.</summary>
void TableNextColumn();
/// <summary>Close the most recent <see cref="BeginTable"/>.</summary>
void EndTable();
/// <summary>
/// A single-line text input that submits on Enter. Returns
/// <c>true</c> on the frame the user pressed Enter; in that case
/// <paramref name="submitted"/> is the value entered and the
/// implementation clears <paramref name="buffer"/> for the next
/// frame. On every other frame returns <c>false</c> and
/// <paramref name="submitted"/> is null; the implementation may
/// still mutate <paramref name="buffer"/> as the user types.
/// </summary>
/// <param name="maxLen">Maximum characters the buffer may grow to.</param>
bool InputTextSubmit(string label, ref string buffer, int maxLen, out string? submitted);
/// <summary>Insert a small vertical gap.</summary>
void Spacing();
/// <summary>Reserve invisible space of the given size, useful for
/// layout-only padding.</summary>
void Dummy(Vector2 size);
/// <summary>
/// Draw text that wraps at the current content region width.
/// Use for descriptions, error messages, anything multi-line.
/// </summary>
void TextWrapped(string text);
// -- Phase J Tier 3 — scrollable child region for chat-style layouts --
/// <summary>
/// Open a scrollable nested region inside the current window.
/// <paramref name="size"/> follows ImGui semantics: 0 means "fill
/// available", a positive value is absolute pixels, a negative
/// value means "fill available minus this amount". Pass
/// <c>(0, -footerHeight)</c> to reserve space at the bottom for
/// fixed elements (separator + input field) so the inner content
/// scrolls but the footer stays put across window resizes.
/// </summary>
bool BeginChild(string id, Vector2 size, bool border = false);
/// <summary>Close the most recent <see cref="BeginChild"/>.</summary>
void EndChild();
/// <summary>
/// Approximate single-line widget height including ImGui's frame
/// padding + item spacing. Panels use this to compute footer
/// reservations for <see cref="BeginChild"/> (e.g. one input
/// field + spacing). Backend-friendly because it returns a
/// scalar that the custom retail-look toolkit can also expose.
/// </summary>
float FrameHeightWithSpacing();
/// <summary>
/// Scroll the current scroll region so that the given fractional
/// vertical position is visible, where 0 = top and 1 = bottom.
/// Typical usage: call with <c>1.0f</c> at the bottom of a
/// chat-tail render block to keep the latest line visible when
/// new entries arrive. No-op when no new content was emitted —
/// callers are expected to skip the call when they don't want
/// to force a scroll.
/// </summary>
void SetScrollHereY(float ratio);
/// <summary>
/// Request keyboard focus for the NEXT widget rendered. Used by
/// <c>ChatPanel</c> when Tab fires <c>ToggleChatEntry</c> — the
/// chat input gets focused programmatically so the user can begin
/// typing without clicking the field.
/// </summary>
void SetKeyboardFocusHere();
// -- Phase K.3 — top-of-screen main menu bar -------------------------
/// <summary>
/// Open the top-of-screen main menu bar. Returns true if the bar is
/// visible (top-level menus go inside that branch). Always pair with
/// <see cref="EndMainMenuBar"/> when the call returned true.
/// </summary>
bool BeginMainMenuBar();
/// <summary>Close the menu bar opened by <see cref="BeginMainMenuBar"/>.</summary>
void EndMainMenuBar();
/// <summary>
/// Open a top-level menu within a menu bar. Returns true if the menu
/// is open — the caller emits <see cref="MenuItem"/> entries inside
/// that branch, then calls <see cref="EndMenu"/>.
/// </summary>
bool BeginMenu(string label);
/// <summary>Close the menu opened by <see cref="BeginMenu"/>.</summary>
void EndMenu();
/// <summary>
/// A clickable menu item with optional shortcut hint (e.g.
/// <c>"F11"</c>) drawn right-aligned. Returns true on the single
/// frame the user clicks the item; false otherwise.
/// </summary>
bool MenuItem(string label, string? shortcut = null);
}