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

90 lines
3.4 KiB
C#

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;
/// <summary>
/// Single-line text input. The display matches the React design's
/// <c>input[type=text]</c>: 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 <see cref="CodexInput.TextEnteredThisFrame"/>;
/// the parent screen subscribes to <c>Window.TextInput</c> in Initialize
/// and routes them through <see cref="CodexInput.OnTextInput"/>. Backspace,
/// Enter and arrow keys are handled in <see cref="Update"/>.
/// </summary>
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<string>? 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<string>? 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);
}
}
}