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 } /// /// 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. /// 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); } }