using FontStashSharp; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Theriapolis.Game.CodexUI.Core; namespace Theriapolis.Game.CodexUI.Widgets; /// /// Selectable card. The clade / species / class / background pickers each /// render a grid of these. Visual states track the React design: /// - Default: rule-coloured 1-px border, parchment fill. /// - Hover: gilded border, subtle gild halo overlay. /// - Selected: seal-red border + inner glow + corner wax-seal accent. /// /// Click toggles selection by calling ; the parent step /// owns the "what is selected" state and rebuilds. The card's content tree /// is whatever child is supplied — typically a small /// Column with name + meta + chips. /// public sealed class CodexCard : CodexWidget { public CodexWidget? Content { get; set; } public bool IsSelected { get; set; } public System.Action? OnClick { get; set; } public Texture2D? CornerSigil { get; set; } public string? CornerLetter { get; set; } // overlay glyph drawn on the sigil placeholder private readonly CodexAtlas _atlas; private bool _hovered; private const int Pad = CodexDensity.CardPad; public CodexCard(CodexAtlas atlas, CodexWidget? content = null, bool selected = false, System.Action? onClick = null) { _atlas = atlas; Content = content; if (content is not null) content.Parent = this; IsSelected = selected; OnClick = onClick; } protected override Point MeasureCore(Point available) { if (Content is null) return new Point(System.Math.Min(CodexDensity.CardWidth, available.X), 80); var inner = new Point(available.X - Pad * 2, available.Y - Pad * 2); var s = Content.Measure(inner); return new Point(s.X + Pad * 2, s.Y + Pad * 2); } protected override void ArrangeCore(Rectangle bounds) { Content?.Arrange(new Rectangle( bounds.X + Pad, bounds.Y + Pad, bounds.Width - Pad * 2, bounds.Height - Pad * 2)); } public override void Update(GameTime gt, CodexInput input) { _hovered = ContainsPoint(input.MousePosition); if (_hovered && input.LeftJustReleased) OnClick?.Invoke(); Content?.Update(gt, input); } public override void Draw(SpriteBatch sb, GameTime gt) { // Body fill — parchment shade sb.Draw(_atlas.Pixel, Bounds, CodexColors.Bg2); // Subtle top-down lift overlay (2-px gradient strip) to match `linear-gradient(180deg, rgba(255,250,235,0.05), transparent 30%)`. sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Y, Bounds.Width, 2), new Color(255, 250, 235, 18)); // Border colour by state. Color border = IsSelected ? CodexColors.Seal : (_hovered ? CodexColors.Gild : CodexColors.Rule); int thickness = IsSelected ? 2 : 1; DrawBorder(sb, Bounds, border, thickness); if (IsSelected) { // Inner glow strip — 1px inside the border. DrawBorder(sb, new Rectangle(Bounds.X + thickness, Bounds.Y + thickness, Bounds.Width - thickness * 2, Bounds.Height - thickness * 2), new Color(border.R, border.G, border.B, (byte)40), 1); // Corner wax-seal accent int sealSize = 28; sb.Draw(_atlas.WaxSeal, new Rectangle(Bounds.Right - sealSize / 2 - 4, Bounds.Y - sealSize / 2 + 4, sealSize, sealSize), Color.White); } else if (_hovered) { sb.Draw(_atlas.Pixel, Bounds, new Color(CodexColors.Gild.R, CodexColors.Gild.G, CodexColors.Gild.B, (byte)10)); } Content?.Draw(sb, gt); } private void DrawBorder(SpriteBatch sb, Rectangle r, Color c, int t) { sb.Draw(_atlas.Pixel, new Rectangle(r.X, r.Y, r.Width, t), c); sb.Draw(_atlas.Pixel, new Rectangle(r.X, r.Bottom - t, r.Width, t), c); sb.Draw(_atlas.Pixel, new Rectangle(r.X, r.Y, t, r.Height), c); sb.Draw(_atlas.Pixel, new Rectangle(r.Right - t, r.Y, t, r.Height), c); } } /// /// 7-step horizontal stepper at the top of the wizard. Each step exposes a /// locked / active / complete state with the matching marker (✕ / Roman / /// ✓). Clicking a non-locked step navigates there. /// public sealed class CodexStepper : CodexWidget { public string[] Names { get; } public int Current { get; set; } public bool[] Complete { get; set; } public bool[] Locked { get; set; } public System.Action? OnPick { get; set; } private readonly CodexAtlas _atlas; private readonly SpriteFontBase _romanFont = CodexFonts.DisplayMedium; private readonly SpriteFontBase _labelFont = CodexFonts.MonoTagSmall; public CodexStepper(string[] names, CodexAtlas atlas) { Names = names; Complete = new bool[names.Length]; Locked = new bool[names.Length]; _atlas = atlas; } protected override Point MeasureCore(Point available) => new(available.X, 64); protected override void ArrangeCore(Rectangle bounds) { } private static readonly string[] Roman = new[] { "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX" }; public override void Update(GameTime gt, CodexInput input) { if (!input.LeftJustReleased) return; int colW = Bounds.Width / Names.Length; for (int i = 0; i < Names.Length; i++) { var cell = new Rectangle(Bounds.X + i * colW, Bounds.Y, colW, Bounds.Height); if (cell.Contains(input.MousePosition) && !Locked[i] && i != Current) { OnPick?.Invoke(i); return; } } } public override void Draw(SpriteBatch sb, GameTime gt) { // Opaque parchment background so the stepper masks any body // scroll-overflow that drew under it. sb.Draw(_atlas.Pixel, Bounds, CodexColors.Bg); // Top + bottom rule sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Y, Bounds.Width, 1), CodexColors.Rule); sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Bottom - 1, Bounds.Width, 1), CodexColors.Rule); sb.Draw(_atlas.Pixel, Bounds, new Color(0, 0, 0, 6)); int colW = Bounds.Width / Names.Length; for (int i = 0; i < Names.Length; i++) { var cell = new Rectangle(Bounds.X + i * colW, Bounds.Y, colW, Bounds.Height); // Vertical separator between cells. if (i < Names.Length - 1) sb.Draw(_atlas.Pixel, new Rectangle(cell.Right - 1, cell.Y + 4, 1, cell.Height - 8), new Color(CodexColors.Rule.R, CodexColors.Rule.G, CodexColors.Rule.B, (byte)128)); bool isCurrent = i == Current; bool isComplete = Complete[i] && !isCurrent; bool isLocked = Locked[i]; // Numeral / mark string mark = isLocked ? "✕" : (isComplete ? "✓" : Roman[i]); Color numColor = isLocked ? CodexColors.InkMute : isComplete ? CodexColors.Seal : isCurrent ? CodexColors.Ink : CodexColors.InkMute; var ms = _romanFont.MeasureString(mark); float numX = cell.X + (cell.Width - ms.X) / 2f; float numY = cell.Y + 12; _romanFont.DrawText(sb, mark, new Vector2(numX, numY), numColor); // Step name var ls = _labelFont.MeasureString(Names[i]); float lx = cell.X + (cell.Width - ls.X) / 2f; float ly = cell.Y + cell.Height - _labelFont.LineHeight - 8; Color labelColor = isLocked ? new Color(CodexColors.InkMute.R, CodexColors.InkMute.G, CodexColors.InkMute.B, (byte)115) : isCurrent ? CodexColors.Ink : CodexColors.InkMute; _labelFont.DrawText(sb, Names[i].ToUpperInvariant(), new Vector2(lx, ly), labelColor); if (isCurrent) sb.Draw(_atlas.Pixel, new Rectangle((int)(cell.X + cell.Width * 0.14f), cell.Bottom - 2, (int)(cell.Width * 0.72f), 2), CodexColors.Gild); } } }