Files
TheriapolisV3/Theriapolis.Game/CodexUI/Widgets/CodexCheckboxRow.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

125 lines
5.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 CheckboxState { Default, Checked, LockedFromBg, Unavailable }
/// <summary>
/// One row of the skill picker. Visual states mirror the React design:
/// - Default: small ink-mute checkbox, hover gilds
/// - Checked: seal-red filled checkbox + ✓
/// - LockedFromBg: gild-filled checkbox + ✓ (sealed by background, can't toggle)
/// - Unavailable: dashed underline, faded text — class doesn't offer it
/// Click toggles only when state is Default or Checked.
/// </summary>
public sealed class CodexCheckboxRow : CodexWidget
{
public string Label { get; set; }
public string SourceTag { get; set; }
public CheckboxState State { get; set; }
public System.Action? OnClick { get; set; }
public System.Action? OnHover { get; set; }
private readonly CodexAtlas _atlas;
private readonly SpriteFontBase _font = CodexFonts.SerifBody;
private readonly SpriteFontBase _tagFont = CodexFonts.MonoTagSmall;
private bool _hovered;
public CodexCheckboxRow(string label, string sourceTag, CheckboxState state, CodexAtlas atlas)
{
Label = label;
SourceTag = sourceTag;
State = state;
_atlas = atlas;
}
protected override Point MeasureCore(Point available) => new(available.X, 28);
protected override void ArrangeCore(Rectangle bounds) { }
public override void Update(GameTime gt, CodexInput input)
{
_hovered = ContainsPoint(input.MousePosition);
// OnHover fires every frame the cursor is over the row, not just
// on hover-enter. The popover is shown only while a trigger calls
// Show() each frame (CodexHoverPopover.IsShown decays in one tick
// when no trigger requests it), so a single transition-only call
// would flash the popover for one frame and then hide it.
if (_hovered) OnHover?.Invoke();
if (_hovered && input.LeftJustReleased)
{
if (State == CheckboxState.Default || State == CheckboxState.Checked) OnClick?.Invoke();
}
}
public override void Draw(SpriteBatch sb, GameTime gt)
{
// Bottom rule
sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Bottom - 1, Bounds.Width, 1),
new Color(CodexColors.Rule.R, CodexColors.Rule.G, CodexColors.Rule.B, (byte)80));
// Checkbox
int boxSize = 18;
var box = new Rectangle(Bounds.X + 2, Bounds.Y + (Bounds.Height - boxSize) / 2, boxSize, boxSize);
Color boxFill, boxBorder, checkColor = CodexColors.Bg;
switch (State)
{
case CheckboxState.Checked:
boxFill = CodexColors.Seal;
boxBorder = CodexColors.Seal;
break;
case CheckboxState.LockedFromBg:
boxFill = CodexColors.Gild;
boxBorder = CodexColors.Gild;
break;
case CheckboxState.Unavailable:
boxFill = Color.Transparent;
boxBorder = new Color(CodexColors.InkMute.R, CodexColors.InkMute.G, CodexColors.InkMute.B, (byte)90);
break;
default:
boxFill = Color.Transparent;
boxBorder = _hovered ? CodexColors.Gild : CodexColors.InkMute;
break;
}
sb.Draw(_atlas.Pixel, box, boxFill);
DrawBorder(sb, box, boxBorder, 1);
if (State == CheckboxState.Checked || State == CheckboxState.LockedFromBg)
{
string mark = "✓";
var s = _font.MeasureString(mark);
_font.DrawText(sb, mark, new Vector2(box.X + (box.Width - s.X) / 2f, box.Y + (box.Height - _font.LineHeight) / 2f),
checkColor);
}
// Label text
Color labelColor = State switch
{
CheckboxState.Checked => CodexColors.Seal,
CheckboxState.LockedFromBg => CodexColors.Gild,
CheckboxState.Unavailable => new Color(CodexColors.InkMute.R, CodexColors.InkMute.G, CodexColors.InkMute.B, (byte)90),
_ => _hovered ? CodexColors.Gild : CodexColors.Ink,
};
_font.DrawText(sb, Label, new Vector2(box.Right + 8, Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f), labelColor);
// Right-aligned source tag
if (!string.IsNullOrEmpty(SourceTag))
{
var ts = _tagFont.MeasureString(SourceTag);
_tagFont.DrawText(sb, SourceTag.ToUpperInvariant(),
new Vector2(Bounds.Right - ts.X - 4, Bounds.Y + (Bounds.Height - _tagFont.LineHeight) / 2f),
CodexColors.InkMute);
}
}
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);
}
}