diff --git a/src/AcDream.UI.Abstractions/IPanelRenderer.cs b/src/AcDream.UI.Abstractions/IPanelRenderer.cs
index 1c0cb2c..3656d75 100644
--- a/src/AcDream.UI.Abstractions/IPanelRenderer.cs
+++ b/src/AcDream.UI.Abstractions/IPanelRenderer.cs
@@ -235,4 +235,31 @@ public interface IPanelRenderer
/// frame the user clicks the item; false otherwise.
///
bool MenuItem(string label, string? shortcut = null);
+
+ // -- Tab bar (Settings panel + future tabbed surfaces) ---------------
+
+ ///
+ /// Open a tab bar inside the current window. Returns true
+ /// when the bar is visible — only emit
+ /// calls inside that branch. Always pair with
+ /// when the call returned true. Retail had
+ /// tab bars in the Options UIs (gmGameplayOptionsUI etc), so
+ /// this primitive must be expressible by the future custom
+ /// retail-look backend.
+ ///
+ bool BeginTabBar(string id);
+
+ /// Close the tab bar opened by .
+ void EndTabBar();
+
+ ///
+ /// Begin a single tab inside an open .
+ /// Returns true when the tab is the currently selected one
+ /// — only render this tab's content in that branch. Always pair
+ /// with when the call returned true.
+ ///
+ bool BeginTabItem(string label);
+
+ /// Close the tab opened by .
+ void EndTabItem();
}
diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs
index 841b394..7def104 100644
--- a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs
+++ b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs
@@ -5,25 +5,23 @@ using AcDream.UI.Abstractions.Input;
namespace AcDream.UI.Abstractions.Panels.Settings;
///
-/// K.3: in-game Settings panel for click-to-rebind keymap editing.
-/// Hidden by default; opens via F11 (which fires the
-/// action) or via the
-/// View → Settings entry on the main menu bar.
+/// In-game Settings panel — F11 toggle (or View → Settings on the main
+/// menu bar). Hidden by default. Tabbed: Keybinds (Phase K), then
+/// Display / Audio / Gameplay / Chat / Character (filling in over the
+/// L.x sub-phases).
///
///
-/// Layout: top row of action buttons (Save / Cancel / Reset all), then
-/// a sequence of sections
-/// matching the retail keymap categories (Movement / Postures / Camera /
-/// Combat / UI panels / Chat / Hotbar / Emotes). Each row inside a
-/// section: action name, current binding(s) summary, "Rebind" button,
-/// per-action "Reset" button. When a rebind is in progress the Rebind
-/// button label changes to "Press a key... (Esc to cancel)".
+/// Top of the panel: Save / Cancel / Reset-all action buttons (global
+/// across all tabs). When is
+/// non-null, a confirmation prompt is rendered above those buttons
+/// (Yes — Reassign / No — Keep existing).
///
///
///
-/// When is non-null, a
-/// confirmation prompt is rendered ABOVE the rest of the panel (Yes —
-/// Reassign / No — Keep existing).
+/// Below the action row a tab bar selects between the six categories.
+/// Only the Keybinds tab is implemented today; the other five render
+/// "Coming soon" placeholders so the structure the user approved in the
+/// design brainstorm is visible immediately.
///
///
public sealed class SettingsPanel : IPanel
@@ -42,7 +40,7 @@ public sealed class SettingsPanel : IPanel
public string Title => "Settings";
///
- /// K.3: hidden by default — opened via F11 / View menu.
+ /// Hidden by default — opened via F11 / View menu.
public bool IsVisible { get; set; } = false;
///
@@ -67,7 +65,7 @@ public sealed class SettingsPanel : IPanel
renderer.Separator();
}
- // Top action buttons.
+ // Top action buttons. Global across all tabs.
if (renderer.Button("Save changes")) _vm.Save();
renderer.SameLine();
if (renderer.Button("Cancel changes")) _vm.Cancel();
@@ -76,7 +74,51 @@ public sealed class SettingsPanel : IPanel
renderer.Separator();
- // Sections (retail keymap categories).
+ if (renderer.BeginTabBar("settings.tabs"))
+ {
+ if (renderer.BeginTabItem("Keybinds"))
+ {
+ RenderKeybindsTab(renderer);
+ renderer.EndTabItem();
+ }
+ if (renderer.BeginTabItem("Display"))
+ {
+ RenderPlaceholder(renderer, "Display");
+ renderer.EndTabItem();
+ }
+ if (renderer.BeginTabItem("Audio"))
+ {
+ RenderPlaceholder(renderer, "Audio");
+ renderer.EndTabItem();
+ }
+ if (renderer.BeginTabItem("Gameplay"))
+ {
+ RenderPlaceholder(renderer, "Gameplay");
+ renderer.EndTabItem();
+ }
+ if (renderer.BeginTabItem("Chat"))
+ {
+ RenderPlaceholder(renderer, "Chat");
+ renderer.EndTabItem();
+ }
+ if (renderer.BeginTabItem("Character"))
+ {
+ RenderPlaceholder(renderer, "Character");
+ renderer.EndTabItem();
+ }
+ renderer.EndTabBar();
+ }
+
+ renderer.End();
+ }
+
+ ///
+ /// Render the Keybinds tab — eight collapsing-header sections matching
+ /// the retail keymap categories. Phase K shipped this content; the
+ /// only thing that changed is the wrapping tab item.
+ ///
+ private void RenderKeybindsTab(IPanelRenderer renderer)
+ {
RenderSection(renderer, "Movement", new[]
{
InputAction.MovementForward, InputAction.MovementBackup,
@@ -136,8 +178,20 @@ public sealed class SettingsPanel : IPanel
InputAction.Cry, InputAction.Laugh, InputAction.Wave,
InputAction.Cheer, InputAction.PointState,
});
+ }
- renderer.End();
+ ///
+ /// Placeholder content shown for tabs whose implementation is still
+ /// pending. Reads as "Coming soon" plus a note about which sub-phase
+ /// is expected to fill it in.
+ ///
+ private static void RenderPlaceholder(IPanelRenderer renderer, string tabName)
+ {
+ renderer.TextWrapped($"{tabName} settings coming soon.");
+ renderer.Spacing();
+ renderer.TextWrapped(
+ "This tab is part of the staged Settings interface rollout. "
+ + "Build order: Display → Audio → Gameplay → Chat → Character.");
}
private void RenderSection(IPanelRenderer renderer, string label, InputAction[] actions)
diff --git a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
index ec00037..d5eb978 100644
--- a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
+++ b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
@@ -193,4 +193,18 @@ public sealed class ImGuiPanelRenderer : IPanelRenderer
=> shortcut is null
? ImGuiNET.ImGui.MenuItem(label)
: ImGuiNET.ImGui.MenuItem(label, shortcut);
+
+ // -- Tab bar -----------------------------------------------------------
+
+ ///
+ public bool BeginTabBar(string id) => ImGuiNET.ImGui.BeginTabBar(id);
+
+ ///
+ public void EndTabBar() => ImGuiNET.ImGui.EndTabBar();
+
+ ///
+ public bool BeginTabItem(string label) => ImGuiNET.ImGui.BeginTabItem(label);
+
+ ///
+ public void EndTabItem() => ImGuiNET.ImGui.EndTabItem();
}
diff --git a/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs b/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs
index f1c8c4d..3706188 100644
--- a/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs
+++ b/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs
@@ -198,4 +198,37 @@ internal sealed class FakePanelRenderer : IPanelRenderer
Calls.Add(("MenuItem", new object?[] { label, shortcut }));
return MenuItemReturns;
}
+
+ // -- Tab bar -----------------------------------------------------------
+
+ /// Pre-set return for .
+ public bool TabBarReturns { get; set; } = true;
+
+ /// The label of the tab the next
+ /// call should report as "selected" (return true). All other tab
+ /// items return false. Defaults to null = the FIRST tab item rendered
+ /// is the selected one.
+ public string? ActiveTabLabel { get; set; }
+
+ private string? _firstTabSeen;
+
+ public bool BeginTabBar(string id)
+ {
+ Calls.Add(("BeginTabBar", new object?[] { id }));
+ _firstTabSeen = null;
+ return TabBarReturns;
+ }
+
+ public void EndTabBar() => Calls.Add(("EndTabBar", Array.Empty