using FontStashSharp; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Theriapolis.Game.CodexUI.Core; namespace Theriapolis.Game.CodexUI.Widgets; public enum CodexButtonVariant { Primary, Ghost, Small } /// /// Codex-styled push button. The three variants match the React design's /// .btn.primary / .btn.ghost / .btn.small: /// - Primary: gilded fill on parchment, used for Confirm + Next-style actions. /// - Ghost: ink border, transparent fill, used for Back + secondary actions. /// - Small: smaller padding/font, used inline (Reroll / Auto-assign / Clear). /// public sealed class CodexButton : CodexWidget { public string Text { get; set; } public CodexButtonVariant Variant { get; set; } public System.Action? OnClick { get; set; } public int? FixedWidth { get; set; } private readonly CodexAtlas _atlas; private readonly SpriteFontBase _font; private bool _hovered; private bool _pressed; public CodexButton(string text, CodexAtlas atlas, CodexButtonVariant variant = CodexButtonVariant.Ghost, System.Action? onClick = null, int? fixedWidth = null) { Text = text; _atlas = atlas; Variant = variant; OnClick = onClick; FixedWidth = fixedWidth; _font = variant == CodexButtonVariant.Small ? CodexFonts.MonoTag : CodexFonts.DisplaySmall; } protected override Point MeasureCore(Point available) { var s = _font.MeasureString(Text); int padX = Variant == CodexButtonVariant.Small ? 12 : 22; int padY = Variant == CodexButtonVariant.Small ? 6 : 10; int w = FixedWidth ?? ((int)s.X + padX * 2); int h = (int)System.MathF.Ceiling(_font.LineHeight) + padY * 2; return new Point(System.Math.Min(w, available.X), h); } protected override void ArrangeCore(Rectangle bounds) { } public override void Update(GameTime gt, CodexInput input) { bool wasHovered = _hovered; _hovered = ContainsPoint(input.MousePosition); if (_hovered && input.LeftJustPressed) _pressed = true; if (input.LeftJustReleased) { if (_pressed && _hovered && Enabled) OnClick?.Invoke(); _pressed = false; } } public override void Draw(SpriteBatch sb, GameTime gt) { Color fill, border, textColor; switch (Variant) { case CodexButtonVariant.Primary: fill = _hovered ? CodexColors.Seal2 : CodexColors.Seal; border = CodexColors.Seal2; textColor = CodexColors.Bg; break; case CodexButtonVariant.Ghost: fill = _hovered ? CodexColors.Ink : CodexColors.Bg; border = CodexColors.Ink; textColor = _hovered ? CodexColors.Bg : CodexColors.Ink; break; default: fill = _hovered ? CodexColors.Bg2 : CodexColors.Bg; border = CodexColors.Rule; textColor = CodexColors.InkSoft; break; } if (!Enabled) { // 40% opacity per .btn[disabled] fill = new Color(fill.R, fill.G, fill.B, (byte)(fill.A * 0.4f)); textColor = new Color(textColor.R, textColor.G, textColor.B, (byte)(textColor.A * 0.6f)); } // Body fill sb.Draw(_atlas.Pixel, Bounds, fill); // 1-px border outline DrawBorder(sb, Bounds, border, 1); var s = _font.MeasureString(Text); float tx = Bounds.X + (Bounds.Width - s.X) / 2f; float ty = Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f; _font.DrawText(sb, Text, new Vector2(tx, ty), textColor); } 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); } }