Files
TheriapolisV3/Theriapolis.Game/CodexUI/Widgets/CodexButton.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

112 lines
4.1 KiB
C#

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 }
/// <summary>
/// Codex-styled push button. The three variants match the React design's
/// <c>.btn.primary / .btn.ghost / .btn.small</c>:
/// - 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).
/// </summary>
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);
}
}