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>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user