b451f83174
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>
125 lines
5.1 KiB
C#
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);
|
|
}
|
|
}
|