using FontStashSharp; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Theriapolis.Game.CodexUI.Core; namespace Theriapolis.Game.CodexUI.Widgets; /// /// Single-line text input. The display matches the React design's /// input[type=text]: serif-display font, transparent background, /// gilded underline rule that lights up while focused. Used for the name /// field in the Sign step. /// /// Receives characters via ; /// the parent screen subscribes to Window.TextInput in Initialize /// and routes them through . Backspace, /// Enter and arrow keys are handled in . /// public sealed class CodexTextBox : CodexWidget { public string Text { get; set; } = ""; public string Placeholder { get; set; } = ""; public bool IsFocused { get; set; } public int? FixedWidth { get; set; } public System.Action? OnChanged { get; set; } private readonly CodexAtlas _atlas; private readonly SpriteFontBase _font; private float _caretBlink; public CodexTextBox(string initial, CodexAtlas atlas, int? fixedWidth = null, System.Action? onChanged = null) { Text = initial; _atlas = atlas; FixedWidth = fixedWidth; OnChanged = onChanged; _font = CodexFonts.DisplayMedium; } protected override Point MeasureCore(Point available) { int w = FixedWidth ?? System.Math.Min(480, available.X); int h = (int)System.MathF.Ceiling(_font.LineHeight) + 14; return new Point(w, h); } protected override void ArrangeCore(Rectangle bounds) { } public override void Update(GameTime gt, CodexInput input) { if (input.LeftJustPressed) IsFocused = ContainsPoint(input.MousePosition); _caretBlink = (_caretBlink + (float)gt.ElapsedGameTime.TotalSeconds) % 1f; if (!IsFocused) return; bool changed = false; if (!string.IsNullOrEmpty(input.TextEnteredThisFrame)) { Text += input.TextEnteredThisFrame; changed = true; } if (input.KeyJustPressed(Keys.Back) && Text.Length > 0) { Text = Text.Substring(0, Text.Length - 1); changed = true; } if (input.KeyJustPressed(Keys.Enter)) IsFocused = false; if (changed) OnChanged?.Invoke(Text); } public override void Draw(SpriteBatch sb, GameTime gt) { // Background — none (transparent) per design; we just paint the underline. Color underline = IsFocused ? CodexColors.Gild : CodexColors.Rule; sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Bottom - 2, Bounds.Width, 2), underline); bool empty = string.IsNullOrEmpty(Text); string display = empty ? Placeholder : Text; Color textColor = empty ? CodexColors.InkMute : CodexColors.Ink; _font.DrawText(sb, display, new Vector2(Bounds.X + 4, Bounds.Y + 6), textColor); // Caret blink — top-aligned, follows the end of the text. if (IsFocused && _caretBlink < 0.5f) { float caretX = Bounds.X + 4 + _font.MeasureString(Text).X; sb.Draw(_atlas.Pixel, new Rectangle((int)caretX, Bounds.Y + 6, 1, (int)_font.LineHeight), CodexColors.Gild); } } }