using System.Collections.Generic; using System.Linq; using AcDream.UI.Abstractions.Input; namespace AcDream.UI.Abstractions.Panels.Settings; /// /// 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). /// /// /// 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). /// /// /// /// 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 { private readonly SettingsVM _vm; public SettingsPanel(SettingsVM vm) { _vm = vm ?? throw new System.ArgumentNullException(nameof(vm)); } /// public string Id => "acdream.settings"; /// public string Title => "Settings"; /// /// Hidden by default — opened via F11 / View menu. public bool IsVisible { get; set; } = false; /// public void Render(PanelContext ctx, IPanelRenderer renderer) { if (!renderer.Begin(Title)) { renderer.End(); return; } // Conflict prompt — modal-ish row at top of the panel. if (_vm.PendingConflict is { } conflict) { renderer.TextWrapped( $"'{ChordLabel(conflict.NewChord)}' is already bound to " + $"{conflict.ConflictingAction}. Reassign it to " + $"{conflict.NewAction}?"); if (renderer.Button("Yes — Reassign")) _vm.ResolveConflict(replace: true); renderer.SameLine(); if (renderer.Button("No — Keep existing")) _vm.ResolveConflict(replace: false); renderer.Separator(); } // Top action buttons. Global across all tabs. if (renderer.Button("Save changes")) _vm.Save(); renderer.SameLine(); if (renderer.Button("Cancel changes")) _vm.Cancel(); renderer.SameLine(); if (renderer.Button("Reset all to retail defaults")) _vm.ResetAllToDefaults(); renderer.Separator(); if (renderer.BeginTabBar("settings.tabs")) { if (renderer.BeginTabItem("Keybinds")) { RenderKeybindsTab(renderer); renderer.EndTabItem(); } if (renderer.BeginTabItem("Display")) { RenderDisplayTab(renderer); 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, InputAction.MovementTurnLeft, InputAction.MovementTurnRight, InputAction.MovementStrafeLeft, InputAction.MovementStrafeRight, InputAction.MovementJump, InputAction.MovementStop, InputAction.MovementWalkMode, InputAction.MovementRunLock, }); RenderSection(renderer, "Postures", new[] { InputAction.Ready, InputAction.Sitting, InputAction.Crouch, InputAction.Sleeping, }); RenderSection(renderer, "Camera", new[] { InputAction.CameraActivateAlternateMode, InputAction.CameraInstantMouseLook, InputAction.CameraRotateLeft, InputAction.CameraRotateRight, InputAction.CameraRotateUp, InputAction.CameraRotateDown, InputAction.CameraMoveToward, InputAction.CameraMoveAway, InputAction.CameraViewDefault, InputAction.CameraViewFirstPerson, InputAction.CameraViewLookDown, InputAction.CameraViewMapMode, }); RenderSection(renderer, "Combat", new[] { InputAction.CombatToggleCombat, InputAction.CombatDecreaseAttackPower, InputAction.CombatIncreaseAttackPower, InputAction.CombatLowAttack, InputAction.CombatMediumAttack, InputAction.CombatHighAttack, InputAction.CombatAimLow, InputAction.CombatAimMedium, InputAction.CombatAimHigh, InputAction.CombatPrevSpellTab, InputAction.CombatNextSpellTab, InputAction.CombatPrevSpell, InputAction.CombatNextSpell, InputAction.CombatCastCurrentSpell, }); RenderSection(renderer, "UI panels", new[] { InputAction.ToggleHelp, InputAction.ToggleAllegiancePanel, InputAction.ToggleFellowshipPanel, InputAction.ToggleSpellbookPanel, InputAction.ToggleSpellComponentsPanel, InputAction.ToggleAttributesPanel, InputAction.ToggleSkillsPanel, InputAction.ToggleWorldPanel, InputAction.ToggleOptionsPanel, InputAction.ToggleInventoryPanel, InputAction.SelectionExamine, InputAction.UseSelected, InputAction.EscapeKey, InputAction.LOGOUT, }); RenderSection(renderer, "Chat", new[] { InputAction.ToggleChatEntry, InputAction.EnterChatMode, InputAction.ToggleFloatingChatWindow1, InputAction.ToggleFloatingChatWindow2, InputAction.ToggleFloatingChatWindow3, InputAction.ToggleFloatingChatWindow4, }); RenderSection(renderer, "Hotbar", new[] { InputAction.UseQuickSlot_1, InputAction.UseQuickSlot_2, InputAction.UseQuickSlot_3, InputAction.UseQuickSlot_4, InputAction.UseQuickSlot_5, InputAction.UseQuickSlot_6, InputAction.UseQuickSlot_7, InputAction.UseQuickSlot_8, InputAction.UseQuickSlot_9, InputAction.CreateShortcut, }); RenderSection(renderer, "Emotes", new[] { InputAction.Cry, InputAction.Laugh, InputAction.Wave, InputAction.Cheer, InputAction.PointState, }); } /// /// 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."); } /// /// Render the Display tab — resolution / fullscreen / vsync / /// FOV / gamma / show-FPS. FOV + Gamma are live-preview sliders; /// the others apply on Save (matches the brainstorm UX agreement — /// resolution change live would be too jarring). /// private void RenderDisplayTab(IPanelRenderer renderer) { var d = _vm.DisplayDraft; // Resolution dropdown. Index falls back to the highest available // option when the persisted resolution isn't one of the presets // (e.g. user hand-edited settings.json with a non-standard size). var resolutions = DisplaySettings.AvailableResolutions.ToArray(); int idx = System.Array.IndexOf(resolutions, d.Resolution); if (idx < 0) idx = resolutions.Length - 1; if (renderer.Combo("Resolution", ref idx, resolutions)) _vm.SetDisplay(d with { Resolution = resolutions[idx] }); bool fullscreen = d.Fullscreen; if (renderer.Checkbox("Fullscreen", ref fullscreen)) _vm.SetDisplay(d with { Fullscreen = fullscreen }); bool vsync = d.VSync; if (renderer.Checkbox("V-Sync", ref vsync)) _vm.SetDisplay(d with { VSync = vsync }); float fov = d.FieldOfView; if (renderer.SliderFloat("Field of View", ref fov, 30f, 120f)) _vm.SetDisplay(d with { FieldOfView = fov }); float gamma = d.Gamma; if (renderer.SliderFloat("Gamma", ref gamma, 0.5f, 2.0f)) _vm.SetDisplay(d with { Gamma = gamma }); bool showFps = d.ShowFps; if (renderer.Checkbox("Show FPS", ref showFps)) _vm.SetDisplay(d with { ShowFps = showFps }); renderer.Spacing(); renderer.TextWrapped( "Resolution / Fullscreen / V-Sync apply on Save. FOV + Gamma " + "preview live as you drag; Cancel reverts to the saved value."); } private void RenderSection(IPanelRenderer renderer, string label, InputAction[] actions) { // Movement defaults open; other sections collapsed for first-run UX. bool defaultOpen = label == "Movement"; if (!renderer.CollapsingHeader(label, defaultOpen)) return; foreach (var action in actions) { renderer.Text(action.ToString()); renderer.SameLine(); // Current binding(s) summary. var binds = _vm.Draft.ForAction(action).ToList(); string summary = binds.Count == 0 ? "(unbound)" : string.Join(", ", binds.Select(b => ChordLabel(b.Chord))); renderer.Text(summary); renderer.SameLine(); // Rebind button — when a rebind is in progress for THIS // action, the label changes to a "press a key..." prompt. // The "##{action}" suffix gives ImGui a stable per-row id // so multiple "Rebind" buttons don't collide. string buttonLabel = (_vm.RebindInProgress == action) ? $"Press a key... (Esc to cancel)##{action}" : $"Rebind##{action}"; if (renderer.Button(buttonLabel)) { if (binds.Count > 0 && _vm.RebindInProgress is null) _vm.BeginRebind(action, binds[0]); } renderer.SameLine(); if (renderer.Button($"Reset##{action}")) _vm.ResetActionToDefault(action); } } /// /// Render a chord as "Shift+Ctrl+A" / "W" / etc. for the /// row summary + conflict prompt. Joins held modifiers with + /// then the trigger key name. /// private static string ChordLabel(KeyChord chord) { var parts = new List(); if ((chord.Modifiers & ModifierMask.Shift) != 0) parts.Add("Shift"); if ((chord.Modifiers & ModifierMask.Ctrl) != 0) parts.Add("Ctrl"); if ((chord.Modifiers & ModifierMask.Alt) != 0) parts.Add("Alt"); if ((chord.Modifiers & ModifierMask.Win) != 0) parts.Add("Win"); parts.Add(chord.Key.ToString()); return string.Join("+", parts); } }