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);
}
}
}