using System; using System.Collections.Generic; using Godot; namespace Theriapolis.GodotHost.UI.Widgets; /// /// Codex stepper. Mirrors .stepper / .step in the React prototype: /// horizontal grid of N steps, each with a Roman numeral and an uppercased /// name. Per-step state drives colour and the gild-coloured underline: /// Active (current), Complete (✓ + seal), Locked (✕ + dimmed). Click on /// any non-locked step emits . /// /// Lock semantics mirror app.jsx: a step is locked iff some /// earlier step has unmet validation. The owning screen passes a per-step /// array; this widget just renders. /// public partial class CodexStepper : HBoxContainer { public enum StepState { Pending, Active, Complete, Locked } [Signal] public delegate void StepClickedEventHandler(int index); private readonly List _entries = new(); public void SetSteps(IReadOnlyList names, IReadOnlyList states) { if (names.Count != states.Count) throw new ArgumentException("names and states must be the same length"); // Rebuild children from scratch — N is small (<=8) so this is cheap. foreach (var child in GetChildren()) child.QueueFree(); _entries.Clear(); for (int i = 0; i < names.Count; i++) { var entry = BuildStep(i, names[i], states[i], isLast: i == names.Count - 1); AddChild(entry.Container); _entries.Add(entry); } } private StepEntry BuildStep(int index, string name, StepState state, bool isLast) { // Outer cell — VBoxContainer with mouse handling on a button child // so we get press events for click-to-jump. SizeFlagsHorizontal // expand makes each cell take an equal share of the width. var btn = new Button { ThemeTypeVariation = "GhostButton", Flat = true, FocusMode = FocusModeEnum.None, CustomMinimumSize = new Vector2(0, 64), SizeFlagsHorizontal = SizeFlags.ExpandFill, ToggleMode = false, }; // Build a vbox child for two stacked labels. var vbox = new VBoxContainer { MouseFilter = MouseFilterEnum.Ignore, SizeFlagsHorizontal = SizeFlags.ExpandFill, SizeFlagsVertical = SizeFlags.ExpandFill, Alignment = BoxContainer.AlignmentMode.Center, }; btn.AddChild(vbox); var num = new Label { Text = state == StepState.Locked ? "✕" : Roman(index + 1), HorizontalAlignment = HorizontalAlignment.Center, ThemeTypeVariation = "StepperNum", MouseFilter = MouseFilterEnum.Ignore, }; vbox.AddChild(num); var lbl = new Label { Text = name.ToUpperInvariant(), HorizontalAlignment = HorizontalAlignment.Center, ThemeTypeVariation = "StepperName", MouseFilter = MouseFilterEnum.Ignore, }; vbox.AddChild(lbl); ApplyStateColors(num, lbl, state); if (state != StepState.Locked) btn.Pressed += () => EmitSignal(SignalName.StepClicked, index); else btn.Disabled = true; // The active step gets a 2-px gild underline. Implement as a // ColorRect bottom-anchored at -1 within the button. if (state == StepState.Active) { var underline = new ColorRect { Color = TryGetThemeColor("font_color", "Gild") ?? new Color("#b48a3c"), MouseFilter = MouseFilterEnum.Ignore, }; underline.AnchorTop = 1.0f; underline.AnchorBottom = 1.0f; underline.AnchorLeft = 0.14f; underline.AnchorRight = 0.86f; underline.OffsetTop = -2; underline.OffsetBottom = 0; btn.AddChild(underline); } return new StepEntry(btn, num, lbl); } private static void ApplyStateColors(Label num, Label name, StepState state) { // Default theme colours come from the StepperNum/StepperName variations // (ink-mute). State overrides bring active steps to ink and complete // to seal-red. Locked uses the dim default plus reduced opacity. Color? numColor = state switch { StepState.Active => TryGetGlobalThemeColor("Ink", new Color("#2b1d10")), StepState.Complete => TryGetGlobalThemeColor("Seal", new Color("#7a1f12")), _ => null, }; Color? nameColor = state switch { StepState.Active => TryGetGlobalThemeColor("Ink", new Color("#2b1d10")), _ => null, }; if (numColor.HasValue) num.AddThemeColorOverride("font_color", numColor.Value); if (nameColor.HasValue) name.AddThemeColorOverride("font_color", nameColor.Value); if (state == StepState.Complete) num.Text = "✓ " + num.Text; if (state == StepState.Locked) { num.Modulate = new Color(1, 1, 1, 0.45f); name.Modulate = new Color(1, 1, 1, 0.45f); } } private static Color? TryGetGlobalThemeColor(string name, Color fallback) => fallback; private Color? TryGetThemeColor(string property, string variation) { if (HasThemeColor(property, variation)) return GetThemeColor(property, variation); return null; } private static string Roman(int n) => n switch { 1 => "I", 2 => "II", 3 => "III", 4 => "IV", 5 => "V", 6 => "VI", 7 => "VII", 8 => "VIII", 9 => "IX", 10 => "X", _ => n.ToString(), }; private readonly record struct StepEntry(Button Container, Label Num, Label Name); }