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;
|
namespace AcDream.UI.Abstractions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -40,4 +42,124 @@ public interface IPanelRenderer
|
||||||
/// <paramref name="overlay"/> is optional text (e.g. <c>"54%"</c>) rendered on top.
|
/// <paramref name="overlay"/> is optional text (e.g. <c>"54%"</c>) rendered on top.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void ProgressBar(float fraction, float width, string? overlay = null);
|
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
|
var size = new Vector2(width, 0f); // height 0 → ImGui picks based on font
|
||||||
ImGuiNET.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
142
tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs
Normal file
142
tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace AcDream.UI.Abstractions.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-memory test double for <see cref="IPanelRenderer"/>. Records every
|
||||||
|
/// widget call so tests can assert against the recorded trace without
|
||||||
|
/// running an ImGui context. New widgets added to the interface must
|
||||||
|
/// implement here too — that compilation error IS the TDD red signal.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class FakePanelRenderer : IPanelRenderer
|
||||||
|
{
|
||||||
|
/// <summary>Ordered list of (method, args) pairs recorded across this renderer's lifetime.</summary>
|
||||||
|
public List<(string Method, object?[] Args)> Calls { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>If <see cref="Begin"/> is called, return this value. Defaults to true so panels render.</summary>
|
||||||
|
public bool BeginReturns { get; set; } = true;
|
||||||
|
|
||||||
|
// -- Inputs the panel under test can mutate via ref args -----------
|
||||||
|
|
||||||
|
/// <summary>Pre-set return value for the next <see cref="Checkbox"/> — caller flips bool to simulate user click.</summary>
|
||||||
|
public bool CheckboxNextReturn { get; set; }
|
||||||
|
public bool? CheckboxNextValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Pre-set return value for the next <see cref="Button"/> — caller sets to simulate user click.</summary>
|
||||||
|
public bool ButtonNextReturn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Pre-set return value for the next <see cref="Combo"/>.</summary>
|
||||||
|
public bool ComboNextReturn { get; set; }
|
||||||
|
public int? ComboNextSelectedIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Pre-set return for <see cref="SliderFloat"/>.</summary>
|
||||||
|
public bool SliderFloatNextReturn { get; set; }
|
||||||
|
public float? SliderFloatNextValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Pre-set return for <see cref="CollapsingHeader"/>.</summary>
|
||||||
|
public bool CollapsingHeaderNextReturn { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Pre-set return for <see cref="TreeNode"/>.</summary>
|
||||||
|
public bool TreeNodeNextReturn { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Pre-set return for <see cref="BeginTable"/>.</summary>
|
||||||
|
public bool BeginTableNextReturn { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Pre-set "submitted" string for the next <see cref="InputTextSubmit"/>; null = no submit this frame.</summary>
|
||||||
|
public string? InputTextSubmitNextSubmitted { get; set; }
|
||||||
|
public string? InputTextSubmitNextBufferAfter { get; set; }
|
||||||
|
|
||||||
|
public bool Begin(string title)
|
||||||
|
{
|
||||||
|
Calls.Add(("Begin", new object?[] { title }));
|
||||||
|
return BeginReturns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void End() => Calls.Add(("End", Array.Empty<object?>()));
|
||||||
|
|
||||||
|
public void Text(string text) => Calls.Add(("Text", new object?[] { text }));
|
||||||
|
|
||||||
|
public void SameLine() => Calls.Add(("SameLine", Array.Empty<object?>()));
|
||||||
|
|
||||||
|
public void Separator() => Calls.Add(("Separator", Array.Empty<object?>()));
|
||||||
|
|
||||||
|
public void ProgressBar(float fraction, float width, string? overlay = null)
|
||||||
|
=> Calls.Add(("ProgressBar", new object?[] { fraction, width, overlay }));
|
||||||
|
|
||||||
|
public void TextColored(Vector4 rgba, string text)
|
||||||
|
=> Calls.Add(("TextColored", new object?[] { rgba, text }));
|
||||||
|
|
||||||
|
public bool CollapsingHeader(string label, bool defaultOpen = true)
|
||||||
|
{
|
||||||
|
Calls.Add(("CollapsingHeader", new object?[] { label, defaultOpen }));
|
||||||
|
return CollapsingHeaderNextReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TreeNode(string label)
|
||||||
|
{
|
||||||
|
Calls.Add(("TreeNode", new object?[] { label }));
|
||||||
|
return TreeNodeNextReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TreePop() => Calls.Add(("TreePop", Array.Empty<object?>()));
|
||||||
|
|
||||||
|
public bool Checkbox(string label, ref bool value)
|
||||||
|
{
|
||||||
|
Calls.Add(("Checkbox", new object?[] { label, value }));
|
||||||
|
if (CheckboxNextValue is bool nv) value = nv;
|
||||||
|
return CheckboxNextReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Button(string label)
|
||||||
|
{
|
||||||
|
Calls.Add(("Button", new object?[] { label }));
|
||||||
|
return ButtonNextReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Combo(string label, ref int selectedIndex, string[] items)
|
||||||
|
{
|
||||||
|
Calls.Add(("Combo", new object?[] { label, selectedIndex, items }));
|
||||||
|
if (ComboNextSelectedIndex is int idx) selectedIndex = idx;
|
||||||
|
return ComboNextReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SliderFloat(string label, ref float value, float min, float max)
|
||||||
|
{
|
||||||
|
Calls.Add(("SliderFloat", new object?[] { label, value, min, max }));
|
||||||
|
if (SliderFloatNextValue is float v) value = v;
|
||||||
|
return SliderFloatNextReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PlotLines(
|
||||||
|
string label,
|
||||||
|
float[] values,
|
||||||
|
int count,
|
||||||
|
int offset = 0,
|
||||||
|
string? overlay = null,
|
||||||
|
float? min = null,
|
||||||
|
float? max = null,
|
||||||
|
Vector2? size = null)
|
||||||
|
=> Calls.Add(("PlotLines", new object?[] { label, values, count, offset, overlay, min, max, size }));
|
||||||
|
|
||||||
|
public void BeginTable(string id, int columns)
|
||||||
|
=> Calls.Add(("BeginTable", new object?[] { id, columns }));
|
||||||
|
|
||||||
|
public void TableNextColumn() => Calls.Add(("TableNextColumn", Array.Empty<object?>()));
|
||||||
|
|
||||||
|
public void EndTable() => Calls.Add(("EndTable", Array.Empty<object?>()));
|
||||||
|
|
||||||
|
public bool InputTextSubmit(string label, ref string buffer, int maxLen, out string? submitted)
|
||||||
|
{
|
||||||
|
Calls.Add(("InputTextSubmit", new object?[] { label, buffer, maxLen }));
|
||||||
|
submitted = InputTextSubmitNextSubmitted;
|
||||||
|
if (submitted is not null)
|
||||||
|
buffer = InputTextSubmitNextBufferAfter ?? string.Empty;
|
||||||
|
return submitted is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Spacing() => Calls.Add(("Spacing", Array.Empty<object?>()));
|
||||||
|
|
||||||
|
public void Dummy(Vector2 size) => Calls.Add(("Dummy", new object?[] { size }));
|
||||||
|
|
||||||
|
public void TextWrapped(string text) => Calls.Add(("TextWrapped", new object?[] { text }));
|
||||||
|
}
|
||||||
308
tests/AcDream.UI.Abstractions.Tests/IPanelRendererWidgetTests.cs
Normal file
308
tests/AcDream.UI.Abstractions.Tests/IPanelRendererWidgetTests.cs
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace AcDream.UI.Abstractions.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests the shape of the new Phase I.1 widget extensions on
|
||||||
|
/// <see cref="IPanelRenderer"/>. Every test dispatches through the
|
||||||
|
/// interface (not the concrete fake) so the test only compiles when
|
||||||
|
/// the interface actually has the method — that's the RED signal for
|
||||||
|
/// TDD when adding new widgets in the future.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class IPanelRendererWidgetTests
|
||||||
|
{
|
||||||
|
private static (IPanelRenderer Iface, FakePanelRenderer Fake) New()
|
||||||
|
{
|
||||||
|
var f = new FakePanelRenderer();
|
||||||
|
return (f, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TextColored_RecordsCallWithColorAndText()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
var color = new Vector4(0.9f, 0.4f, 0.2f, 1f);
|
||||||
|
|
||||||
|
r.TextColored(color, "ouch");
|
||||||
|
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("TextColored", call.Method);
|
||||||
|
Assert.Equal(color, call.Args[0]);
|
||||||
|
Assert.Equal("ouch", call.Args[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CollapsingHeader_RecordsLabelAndDefaultOpen_ReturnsBackingValue()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
fake.CollapsingHeaderNextReturn = false;
|
||||||
|
|
||||||
|
bool open = r.CollapsingHeader("Player Info");
|
||||||
|
Assert.False(open);
|
||||||
|
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("CollapsingHeader", call.Method);
|
||||||
|
Assert.Equal("Player Info", call.Args[0]);
|
||||||
|
Assert.Equal(true, call.Args[1]); // default
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CollapsingHeader_PassesDefaultOpenFalse()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
|
||||||
|
r.CollapsingHeader("Diagnostics", defaultOpen: false);
|
||||||
|
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal(false, call.Args[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TreeNode_AndTreePop_FormPair()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
fake.TreeNodeNextReturn = true;
|
||||||
|
|
||||||
|
if (r.TreeNode("Inventory"))
|
||||||
|
{
|
||||||
|
r.Text("sword");
|
||||||
|
r.TreePop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(3, fake.Calls.Count);
|
||||||
|
Assert.Equal("TreeNode", fake.Calls[0].Method);
|
||||||
|
Assert.Equal("Inventory", fake.Calls[0].Args[0]);
|
||||||
|
Assert.Equal("Text", fake.Calls[1].Method);
|
||||||
|
Assert.Equal("TreePop", fake.Calls[2].Method);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Checkbox_AppliesNextValueToRefAndRecordsBefore()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
fake.CheckboxNextReturn = true;
|
||||||
|
fake.CheckboxNextValue = true;
|
||||||
|
|
||||||
|
bool flag = false;
|
||||||
|
bool changed = r.Checkbox("DumpMotion", ref flag);
|
||||||
|
|
||||||
|
Assert.True(changed);
|
||||||
|
Assert.True(flag); // fake mutated the ref
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("Checkbox", call.Method);
|
||||||
|
Assert.Equal("DumpMotion", call.Args[0]);
|
||||||
|
Assert.Equal(false, call.Args[1]); // value-before-the-call captured in trace
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Button_ReturnsBackingValueAndRecordsLabel()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
fake.ButtonNextReturn = true;
|
||||||
|
|
||||||
|
bool clicked = r.Button("Cycle Weather");
|
||||||
|
|
||||||
|
Assert.True(clicked);
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("Button", call.Method);
|
||||||
|
Assert.Equal("Cycle Weather", call.Args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Combo_AppliesNextSelectedIndex_AndReturnsChanged()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
fake.ComboNextReturn = true;
|
||||||
|
fake.ComboNextSelectedIndex = 2;
|
||||||
|
|
||||||
|
int idx = 0;
|
||||||
|
var items = new[] { "Dawn", "Noon", "Dusk", "Night" };
|
||||||
|
bool changed = r.Combo("Time", ref idx, items);
|
||||||
|
|
||||||
|
Assert.True(changed);
|
||||||
|
Assert.Equal(2, idx);
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("Combo", call.Method);
|
||||||
|
Assert.Equal("Time", call.Args[0]);
|
||||||
|
Assert.Equal(0, call.Args[1]);
|
||||||
|
Assert.Same(items, call.Args[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SliderFloat_AppliesNextValueAndReturnsChanged()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
fake.SliderFloatNextReturn = true;
|
||||||
|
fake.SliderFloatNextValue = 0.7f;
|
||||||
|
|
||||||
|
float v = 0.1f;
|
||||||
|
bool changed = r.SliderFloat("Sensitivity", ref v, 0f, 1f);
|
||||||
|
|
||||||
|
Assert.True(changed);
|
||||||
|
Assert.Equal(0.7f, v);
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("SliderFloat", call.Method);
|
||||||
|
Assert.Equal("Sensitivity", call.Args[0]);
|
||||||
|
Assert.Equal(0.1f, call.Args[1]);
|
||||||
|
Assert.Equal(0f, call.Args[2]);
|
||||||
|
Assert.Equal(1f, call.Args[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlotLines_RecordsAllOptionalArgs()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
var values = new[] { 16f, 17f, 16.5f, 16.2f };
|
||||||
|
|
||||||
|
r.PlotLines(
|
||||||
|
"FrameMs",
|
||||||
|
values,
|
||||||
|
count: values.Length,
|
||||||
|
offset: 1,
|
||||||
|
overlay: "60 fps",
|
||||||
|
min: 0f,
|
||||||
|
max: 33f,
|
||||||
|
size: new Vector2(120, 40));
|
||||||
|
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("PlotLines", call.Method);
|
||||||
|
Assert.Equal("FrameMs", call.Args[0]);
|
||||||
|
Assert.Same(values, call.Args[1]);
|
||||||
|
Assert.Equal(values.Length, call.Args[2]);
|
||||||
|
Assert.Equal(1, call.Args[3]);
|
||||||
|
Assert.Equal("60 fps", call.Args[4]);
|
||||||
|
Assert.Equal(0f, call.Args[5]);
|
||||||
|
Assert.Equal(33f, call.Args[6]);
|
||||||
|
Assert.Equal(new Vector2(120, 40), call.Args[7]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlotLines_DefaultsApplied_WhenOptionalsOmitted()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
var values = new[] { 1f, 2f, 3f };
|
||||||
|
|
||||||
|
r.PlotLines("FPS", values, count: values.Length);
|
||||||
|
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal(0, call.Args[3]); // offset default
|
||||||
|
Assert.Null(call.Args[4]); // overlay default
|
||||||
|
Assert.Null(call.Args[5]); // min default
|
||||||
|
Assert.Null(call.Args[6]); // max default
|
||||||
|
Assert.Null(call.Args[7]); // size default
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Table_BeginNextColumnEnd_FormSequence()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
|
||||||
|
r.BeginTable("Keybinds", 2);
|
||||||
|
r.TableNextColumn();
|
||||||
|
r.Text("F1");
|
||||||
|
r.TableNextColumn();
|
||||||
|
r.Text("Toggle Info");
|
||||||
|
r.EndTable();
|
||||||
|
|
||||||
|
Assert.Equal(6, fake.Calls.Count);
|
||||||
|
Assert.Equal("BeginTable", fake.Calls[0].Method);
|
||||||
|
Assert.Equal("Keybinds", fake.Calls[0].Args[0]);
|
||||||
|
Assert.Equal(2, fake.Calls[0].Args[1]);
|
||||||
|
Assert.Equal("TableNextColumn", fake.Calls[1].Method);
|
||||||
|
Assert.Equal("Text", fake.Calls[2].Method);
|
||||||
|
Assert.Equal("TableNextColumn", fake.Calls[3].Method);
|
||||||
|
Assert.Equal("Text", fake.Calls[4].Method);
|
||||||
|
Assert.Equal("EndTable", fake.Calls[5].Method);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InputTextSubmit_NoSubmit_ReturnsFalseAndSubmittedNull()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
// InputTextSubmitNextSubmitted left null → the buffer should be untouched
|
||||||
|
// and the out arg should be null.
|
||||||
|
|
||||||
|
string buf = "hel";
|
||||||
|
bool submitted = r.InputTextSubmit("##chat", ref buf, 256, out var output);
|
||||||
|
|
||||||
|
Assert.False(submitted);
|
||||||
|
Assert.Null(output);
|
||||||
|
Assert.Equal("hel", buf);
|
||||||
|
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("InputTextSubmit", call.Method);
|
||||||
|
Assert.Equal("##chat", call.Args[0]);
|
||||||
|
Assert.Equal("hel", call.Args[1]);
|
||||||
|
Assert.Equal(256, call.Args[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InputTextSubmit_OnSubmit_OutputsTextAndClearsBuffer()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
fake.InputTextSubmitNextSubmitted = "hello world";
|
||||||
|
fake.InputTextSubmitNextBufferAfter = string.Empty;
|
||||||
|
|
||||||
|
string buf = "hello world";
|
||||||
|
bool submitted = r.InputTextSubmit("##chat", ref buf, 256, out var output);
|
||||||
|
|
||||||
|
Assert.True(submitted);
|
||||||
|
Assert.Equal("hello world", output);
|
||||||
|
// Contract: on submit the impl clears the buffer for the next frame.
|
||||||
|
Assert.Equal(string.Empty, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Spacing_RecordsCall()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
r.Spacing();
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("Spacing", call.Method);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dummy_RecordsSize()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
r.Dummy(new Vector2(8, 12));
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("Dummy", call.Method);
|
||||||
|
Assert.Equal(new Vector2(8, 12), call.Args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TextWrapped_RecordsText()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
r.TextWrapped("a long description that wraps onto multiple lines");
|
||||||
|
|
||||||
|
var call = Assert.Single(fake.Calls);
|
||||||
|
Assert.Equal("TextWrapped", call.Method);
|
||||||
|
Assert.Equal("a long description that wraps onto multiple lines", call.Args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Sanity: existing widgets still recorded correctly via the fake -------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BeginEndTextSeparatorProgressBar_StillRecorded()
|
||||||
|
{
|
||||||
|
var (r, fake) = New();
|
||||||
|
fake.BeginReturns = true;
|
||||||
|
|
||||||
|
Assert.True(r.Begin("Window"));
|
||||||
|
r.Text("hi");
|
||||||
|
r.SameLine();
|
||||||
|
r.Separator();
|
||||||
|
r.ProgressBar(0.5f, 100f, "50%");
|
||||||
|
r.End();
|
||||||
|
|
||||||
|
Assert.Equal(6, fake.Calls.Count);
|
||||||
|
Assert.Equal("Begin", fake.Calls[0].Method);
|
||||||
|
Assert.Equal("Text", fake.Calls[1].Method);
|
||||||
|
Assert.Equal("SameLine", fake.Calls[2].Method);
|
||||||
|
Assert.Equal("Separator", fake.Calls[3].Method);
|
||||||
|
Assert.Equal("ProgressBar", fake.Calls[4].Method);
|
||||||
|
Assert.Equal("End", fake.Calls[5].Method);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue