diff --git a/src/AcDream.UI.Abstractions/IPanelRenderer.cs b/src/AcDream.UI.Abstractions/IPanelRenderer.cs
index 93e2584..91cc42f 100644
--- a/src/AcDream.UI.Abstractions/IPanelRenderer.cs
+++ b/src/AcDream.UI.Abstractions/IPanelRenderer.cs
@@ -1,3 +1,5 @@
+using System.Numerics;
+
namespace AcDream.UI.Abstractions;
///
@@ -40,4 +42,124 @@ public interface IPanelRenderer
/// is optional text (e.g. "54%") rendered on top.
///
void ProgressBar(float fraction, float width, string? overlay = null);
+
+ // -- Phase I.1 widget extensions ----------------------------------
+
+ ///
+ /// 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.
+ ///
+ void TextColored(Vector4 rgba, string text);
+
+ ///
+ /// Open / close section grouping inside a window. Returns
+ /// true when the section is currently expanded — only render
+ /// the section's contents in that branch.
+ ///
+ /// If the user has never toggled the
+ /// header before, start in this state.
+ bool CollapsingHeader(string label, bool defaultOpen = true);
+
+ ///
+ /// Push an expandable nested node. Pair every successful
+ /// true return with a matching ;
+ /// when this returns false do NOT call .
+ ///
+ bool TreeNode(string label);
+
+ /// Pop a previously-pushed .
+ void TreePop();
+
+ ///
+ /// A boolean toggle. Returns true on the frame the user
+ /// flipped the box (and updates in place);
+ /// returns false the rest of the time.
+ ///
+ bool Checkbox(string label, ref bool value);
+
+ ///
+ /// A clickable button. Returns true on the single frame the
+ /// user clicked it; false every other frame.
+ ///
+ bool Button(string label);
+
+ ///
+ /// A drop-down selector. Returns true on the frame the user
+ /// changed the selection (and updates
+ /// to the new value); false otherwise.
+ ///
+ bool Combo(string label, ref int selectedIndex, string[] items);
+
+ ///
+ /// A horizontal slider clamped to /
+ /// . Returns true on frames where the
+ /// user dragged the value (and updates );
+ /// false when idle.
+ ///
+ bool SliderFloat(string label, ref float value, float min, float max);
+
+ ///
+ /// Time-series line graph (e.g. fps history). The active window is
+ /// [..
+ /// +] interpreted
+ /// as a ring buffer.
+ ///
+ /// Header text shown above the plot.
+ /// Sample buffer (kept by the caller).
+ /// Number of valid samples in the ring.
+ /// Index of the oldest sample in
+ /// .
+ /// Optional centered overlay text (e.g.
+ /// "60 fps").
+ /// Optional fixed lower bound; null = autoscale.
+ /// Optional fixed upper bound; null = autoscale.
+ /// Optional pixel size; null = backend default.
+ void PlotLines(
+ string label,
+ float[] values,
+ int count,
+ int offset = 0,
+ string? overlay = null,
+ float? min = null,
+ float? max = null,
+ Vector2? size = null);
+
+ ///
+ /// Begin a multi-column table. Pair every call with .
+ /// Inside the table, advance to each cell with
+ /// before emitting widgets for it.
+ ///
+ void BeginTable(string id, int columns);
+
+ /// Move to the next cell of the active table.
+ void TableNextColumn();
+
+ /// Close the most recent .
+ void EndTable();
+
+ ///
+ /// A single-line text input that submits on Enter. Returns
+ /// true on the frame the user pressed Enter; in that case
+ /// is the value entered and the
+ /// implementation clears for the next
+ /// frame. On every other frame returns false and
+ /// is null; the implementation may
+ /// still mutate as the user types.
+ ///
+ /// Maximum characters the buffer may grow to.
+ bool InputTextSubmit(string label, ref string buffer, int maxLen, out string? submitted);
+
+ /// Insert a small vertical gap.
+ void Spacing();
+
+ /// Reserve invisible space of the given size, useful for
+ /// layout-only padding.
+ void Dummy(Vector2 size);
+
+ ///
+ /// Draw text that wraps at the current content region width.
+ /// Use for descriptions, error messages, anything multi-line.
+ ///
+ void TextWrapped(string text);
}
diff --git a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
index d1fde2f..e39cc43 100644
--- a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
+++ b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
@@ -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 ---------------------------------
+
+ ///
+ public void TextColored(Vector4 rgba, string text)
+ => ImGuiNET.ImGui.TextColored(rgba, text);
+
+ ///
+ public bool CollapsingHeader(string label, bool defaultOpen = true)
+ => ImGuiNET.ImGui.CollapsingHeader(
+ label,
+ defaultOpen ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None);
+
+ ///
+ public bool TreeNode(string label) => ImGuiNET.ImGui.TreeNode(label);
+
+ ///
+ public void TreePop() => ImGuiNET.ImGui.TreePop();
+
+ ///
+ public bool Checkbox(string label, ref bool value)
+ => ImGuiNET.ImGui.Checkbox(label, ref value);
+
+ ///
+ public bool Button(string label) => ImGuiNET.ImGui.Button(label);
+
+ ///
+ public bool Combo(string label, ref int selectedIndex, string[] items)
+ => ImGuiNET.ImGui.Combo(label, ref selectedIndex, items, items.Length);
+
+ ///
+ public bool SliderFloat(string label, ref float value, float min, float max)
+ => ImGuiNET.ImGui.SliderFloat(label, ref value, min, max);
+
+ ///
+ 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);
+ }
+
+ ///
+ public void BeginTable(string id, int columns)
+ => ImGuiNET.ImGui.BeginTable(id, columns);
+
+ ///
+ public void TableNextColumn() => ImGuiNET.ImGui.TableNextColumn();
+
+ ///
+ public void EndTable() => ImGuiNET.ImGui.EndTable();
+
+ ///
+ 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;
+ }
+
+ ///
+ public void Spacing() => ImGuiNET.ImGui.Spacing();
+
+ ///
+ public void Dummy(Vector2 size) => ImGuiNET.ImGui.Dummy(size);
+
+ ///
+ public void TextWrapped(string text) => ImGuiNET.ImGui.TextWrapped(text);
}
diff --git a/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs b/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs
new file mode 100644
index 0000000..70fcfc3
--- /dev/null
+++ b/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs
@@ -0,0 +1,142 @@
+using System.Numerics;
+
+namespace AcDream.UI.Abstractions.Tests;
+
+///
+/// In-memory test double for . 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.
+///
+internal sealed class FakePanelRenderer : IPanelRenderer
+{
+ /// Ordered list of (method, args) pairs recorded across this renderer's lifetime.
+ public List<(string Method, object?[] Args)> Calls { get; } = new();
+
+ /// If is called, return this value. Defaults to true so panels render.
+ public bool BeginReturns { get; set; } = true;
+
+ // -- Inputs the panel under test can mutate via ref args -----------
+
+ /// Pre-set return value for the next — caller flips bool to simulate user click.
+ public bool CheckboxNextReturn { get; set; }
+ public bool? CheckboxNextValue { get; set; }
+
+ /// Pre-set return value for the next — caller sets to simulate user click.
+ public bool ButtonNextReturn { get; set; }
+
+ /// Pre-set return value for the next .
+ public bool ComboNextReturn { get; set; }
+ public int? ComboNextSelectedIndex { get; set; }
+
+ /// Pre-set return for .
+ public bool SliderFloatNextReturn { get; set; }
+ public float? SliderFloatNextValue { get; set; }
+
+ /// Pre-set return for .
+ public bool CollapsingHeaderNextReturn { get; set; } = true;
+
+ /// Pre-set return for .
+ public bool TreeNodeNextReturn { get; set; } = true;
+
+ /// Pre-set return for .
+ public bool BeginTableNextReturn { get; set; } = true;
+
+ /// Pre-set "submitted" string for the next ; null = no submit this frame.
+ 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