feat(ui): #14 IPanelRenderer widget extension - TextColored, Checkbox, Combo, InputTextSubmit, BeginTable, etc.
Adds 14 widget signatures to IPanelRenderer + ImGuiPanelRenderer impl: TextColored, CollapsingHeader, TreeNode/TreePop, Checkbox, Button, Combo, SliderFloat, PlotLines, BeginTable/TableNextColumn/EndTable, InputTextSubmit (Enter-key submit), Spacing, Dummy, TextWrapped. InputTextSubmit uses ImGuiInputTextFlags.EnterReturnsTrue and clears the buffer + emits via `out submitted` on the frame Enter is pressed. PlotLines passes `ref values[0]` with empty-array guard. CollapsingHeader defaultOpen=true uses ImGuiTreeNodeFlags.DefaultOpen (= 0x20). FakePanelRenderer test double records (Method, Args) tuples and exposes knobs to drive ref/out values. 17 new tests dispatch through IPanelRenderer (not the concrete fake) so tests fail to compile when the interface itself lacks a method - real RED -> GREEN signal. Tests: 26 -> 43 in UI.Abstractions.Tests. Total solution 881 green. Foundation for Phase I.2 (DebugPanel) and I.4 (ChatPanel input field). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
196f883c10
commit
b131514d51
4 changed files with 683 additions and 0 deletions
|
|
@ -1,3 +1,5 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.UI.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -40,4 +42,124 @@ public interface IPanelRenderer
|
|||
/// <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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,4 +39,115 @@ public sealed class ImGuiPanelRenderer : IPanelRenderer
|
|||
var size = new Vector2(width, 0f); // height 0 → ImGui picks based on font
|
||||
ImGuiNET.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty);
|
||||
}
|
||||
|
||||
// -- Phase I.1 widget extensions ---------------------------------
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TextColored(Vector4 rgba, string text)
|
||||
=> ImGuiNET.ImGui.TextColored(rgba, text);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CollapsingHeader(string label, bool defaultOpen = true)
|
||||
=> ImGuiNET.ImGui.CollapsingHeader(
|
||||
label,
|
||||
defaultOpen ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TreeNode(string label) => ImGuiNET.ImGui.TreeNode(label);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TreePop() => ImGuiNET.ImGui.TreePop();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Checkbox(string label, ref bool value)
|
||||
=> ImGuiNET.ImGui.Checkbox(label, ref value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Button(string label) => ImGuiNET.ImGui.Button(label);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Combo(string label, ref int selectedIndex, string[] items)
|
||||
=> ImGuiNET.ImGui.Combo(label, ref selectedIndex, items, items.Length);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SliderFloat(string label, ref float value, float min, float max)
|
||||
=> ImGuiNET.ImGui.SliderFloat(label, ref value, min, max);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PlotLines(
|
||||
string label,
|
||||
float[] values,
|
||||
int count,
|
||||
int offset = 0,
|
||||
string? overlay = null,
|
||||
float? min = null,
|
||||
float? max = null,
|
||||
Vector2? size = null)
|
||||
{
|
||||
// ImGui.NET 1.91.6.1's PlotLines binding takes `ref float values`
|
||||
// (pointer-to-first-element semantics) plus a separate values_count
|
||||
// and values_offset. The "no fixed bound" / "default size" sentinels
|
||||
// are float.MaxValue and Vector2.Zero respectively — we pass those
|
||||
// when the caller leaves the optional args null.
|
||||
if (count <= 0 || values.Length == 0)
|
||||
{
|
||||
// Nothing to plot — emit the label so layout doesn't shift but
|
||||
// skip the native call (ref to values[0] would NRE on empty).
|
||||
ImGuiNET.ImGui.TextUnformatted(label);
|
||||
return;
|
||||
}
|
||||
|
||||
float scaleMin = min ?? float.MaxValue;
|
||||
float scaleMax = max ?? float.MaxValue;
|
||||
Vector2 graphSize = size ?? Vector2.Zero;
|
||||
ImGuiNET.ImGui.PlotLines(
|
||||
label,
|
||||
ref values[0],
|
||||
count,
|
||||
offset,
|
||||
overlay ?? string.Empty,
|
||||
scaleMin,
|
||||
scaleMax,
|
||||
graphSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void BeginTable(string id, int columns)
|
||||
=> ImGuiNET.ImGui.BeginTable(id, columns);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TableNextColumn() => ImGuiNET.ImGui.TableNextColumn();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EndTable() => ImGuiNET.ImGui.EndTable();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool InputTextSubmit(string label, ref string buffer, int maxLen, out string? submitted)
|
||||
{
|
||||
// EnterReturnsTrue: the call returns true on the frame the user
|
||||
// pressed Enter. On every other frame ImGui still mutates `buffer`
|
||||
// as the user types; we just don't surface a submit.
|
||||
bool entered = ImGuiNET.ImGui.InputText(
|
||||
label,
|
||||
ref buffer,
|
||||
(uint)maxLen,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue);
|
||||
if (entered)
|
||||
{
|
||||
submitted = buffer;
|
||||
buffer = string.Empty; // contract: clear for next frame
|
||||
return true;
|
||||
}
|
||||
submitted = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Spacing() => ImGuiNET.ImGui.Spacing();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dummy(Vector2 size) => ImGuiNET.ImGui.Dummy(size);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TextWrapped(string text) => ImGuiNET.ImGui.TextWrapped(text);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue