using FontStashSharp; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Theriapolis.Game.CodexUI.Core; namespace Theriapolis.Game.CodexUI.Widgets; /// /// Single- or multi-line text widget. Wraps to if set; /// otherwise to its arrange width. Color and font are explicit so the same /// widget can render the codex header (DisplayLarge / Ink), an eyebrow /// (MonoTag / InkMute), or a body paragraph (SerifBody / InkSoft). /// public sealed class CodexLabel : CodexWidget { public string Text { get; set; } = ""; public SpriteFontBase Font { get; set; } public Color Color { get; set; } = CodexColors.Ink; public HAlign HAlign { get; set; } = HAlign.Left; /// If set, text wraps when its measured width would exceed this. public int? MaxWidth { get; set; } public CodexLabel(string text, SpriteFontBase font, Color? color = null, HAlign hAlign = HAlign.Left) { Text = text; Font = font; if (color.HasValue) Color = color.Value; HAlign = hAlign; } private string[] _wrappedLines = System.Array.Empty(); protected override Point MeasureCore(Point available) { int wrapW = MaxWidth ?? available.X; _wrappedLines = WrapText(Text, Font, wrapW); int width = 0; foreach (var line in _wrappedLines) { var s = Font.MeasureString(line); if (s.X > width) width = (int)System.MathF.Ceiling(s.X); } int height = (int)System.MathF.Ceiling(Font.LineHeight * (_wrappedLines.Length == 0 ? 1 : _wrappedLines.Length)); return new Point(System.Math.Min(width, wrapW), height); } protected override void ArrangeCore(Rectangle bounds) { } public override void Draw(SpriteBatch sb, GameTime gt) { float y = Bounds.Y; foreach (var line in _wrappedLines) { var s = Font.MeasureString(line); float x = HAlign switch { HAlign.Center => Bounds.X + (Bounds.Width - s.X) / 2f, HAlign.Right => Bounds.X + Bounds.Width - s.X, _ => Bounds.X, }; Font.DrawText(sb, line, new Vector2(x, y), Color); y += Font.LineHeight; } } /// /// Greedy word wrap. Splits on spaces, fits as many words as possible per /// line, hard-breaks oversize words. Honours embedded \n. /// public static string[] WrapText(string text, SpriteFontBase font, int maxWidth) { if (string.IsNullOrEmpty(text)) return new[] { "" }; var lines = new System.Collections.Generic.List(); foreach (var paragraph in text.Split('\n')) { var words = paragraph.Split(' '); var current = new System.Text.StringBuilder(); foreach (var word in words) { string trial = current.Length == 0 ? word : current + " " + word; if (font.MeasureString(trial).X > maxWidth && current.Length > 0) { lines.Add(current.ToString()); current.Clear(); current.Append(word); } else { current.Clear(); current.Append(trial); } } lines.Add(current.ToString()); } return lines.ToArray(); } } /// /// Decorative horizontal rule with a small central diamond glyph. Mirrors /// the React design's .divider / .clade-group-label::after /// hairline + ornament pattern. /// public sealed class CodexOrnamentRule : CodexWidget { public Color RuleColor { get; set; } = CodexColors.Rule; public string? Label { get; set; } public SpriteFontBase Font { get; set; } private readonly CodexAtlas _atlas; public CodexOrnamentRule(CodexAtlas atlas, SpriteFontBase font, string? label = null) { _atlas = atlas; Font = font; Label = label; } protected override Point MeasureCore(Point available) => new(available.X, 16); protected override void ArrangeCore(Rectangle bounds) { } public override void Draw(SpriteBatch sb, GameTime gt) { int midY = Bounds.Y + Bounds.Height / 2; if (!string.IsNullOrEmpty(Label)) { var s = Font.MeasureString(Label); int padX = 12; int textX = Bounds.X + 0; // Left rule before the text. sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, midY, 16, 1), RuleColor); int textXStart = Bounds.X + 16 + padX / 2; Font.DrawText(sb, Label, new Vector2(textXStart, midY - Font.LineHeight / 2f), CodexColors.InkMute); int afterText = textXStart + (int)s.X + padX / 2; sb.Draw(_atlas.Pixel, new Rectangle(afterText, midY, Bounds.Right - afterText, 1), RuleColor); } else { int half = (Bounds.Width - 16) / 2; sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, midY, half, 1), RuleColor); sb.Draw(_atlas.OrnamentDiamond, new Rectangle(Bounds.X + half, midY - 8, 16, 16), CodexColors.Gild); sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X + half + 16, midY, half, 1), RuleColor); } } }