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,89 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user