using System.Numerics; using System.Text; namespace AcDream.App.UI.Layout; /// /// Font-color indices selected by retail ItemExamineUI. The concrete /// colors come from the item report's LayoutDesc property 0x1B. /// public enum ItemAppraisalFontStyle { Normal = 0, Beneficial = 1, Detrimental = 2, } /// /// Separator inserted before an appraisal fragment. This is the retained /// equivalent of ItemExamineUI::AddItemInfo's final argument. /// public enum ItemAppraisalSeparator { None, Line, Paragraph, } /// /// One retail appraisal append operation: prose, separator, and authored /// font-color index. Keeping these separate until shaping preserves style /// when a long fragment wraps. /// public readonly record struct ItemAppraisalFragment( string Text, ItemAppraisalSeparator Separator, ItemAppraisalFontStyle Style); /// /// Immutable item appraisal report in retail append order. /// public sealed class ItemAppraisalReport { public static ItemAppraisalReport Empty { get; } = new([]); public ItemAppraisalReport(IReadOnlyList fragments) { ArgumentNullException.ThrowIfNull(fragments); Fragments = fragments; } public IReadOnlyList Fragments { get; } public bool IsEmpty => Fragments.Count == 0; public override string ToString() { var text = new StringBuilder(); foreach (ItemAppraisalFragment fragment in Fragments) { if (text.Length != 0) { text.Append(fragment.Separator == ItemAppraisalSeparator.Paragraph ? "\n\n" : "\n"); } text.Append(fragment.Text); } return text.ToString(); } } /// /// Port of ItemExamineUI::AddItemInfo @ 0x004AC050/0x004ADCA0. /// Retail appends one newline when is true, /// two otherwise, then applies font DID index zero and the supplied color index. /// internal sealed class ItemAppraisalReportBuilder { private readonly List _fragments = []; public void Line( string value, ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal) => Add(value, sameParagraph: true, style); public void Paragraph( string value, ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal) => Add(value, sameParagraph: false, style); public ItemAppraisalReport Build() => _fragments.Count == 0 ? ItemAppraisalReport.Empty : new ItemAppraisalReport(_fragments.ToArray()); private void Add( string value, bool sameParagraph, ItemAppraisalFontStyle style) { if (string.IsNullOrWhiteSpace(value)) return; _fragments.Add(new ItemAppraisalFragment( value, _fragments.Count == 0 ? ItemAppraisalSeparator.None : sameParagraph ? ItemAppraisalSeparator.Line : ItemAppraisalSeparator.Paragraph, style)); } } /// /// Width-aware projection of retail appraisal fragments into the retained /// line model. /// internal static class ItemAppraisalTextLayout { public static IReadOnlyList Shape( UiText target, ItemAppraisalReport report) { ArgumentNullException.ThrowIfNull(target); ArgumentNullException.ThrowIfNull(report); float maxWidth = Math.Max(1f, target.Width - (2f * target.Padding)); float Measure(string value) => target.DatFont?.MeasureWidth(value) ?? target.Font?.MeasureWidth(value) ?? value.Length * 8f; var lines = new List(); foreach (ItemAppraisalFragment fragment in report.Fragments) { if (lines.Count != 0 && fragment.Separator == ItemAppraisalSeparator.Paragraph) { lines.Add(new UiText.Line(string.Empty, ResolveColor(target, fragment.Style))); } Vector4 color = ResolveColor(target, fragment.Style); string normalized = fragment.Text.Replace( "\\n", "\n", StringComparison.Ordinal); foreach (string logicalLine in normalized.Split('\n')) { if (logicalLine.Length == 0) { lines.Add(new UiText.Line(string.Empty, color)); continue; } foreach (string wrapped in ChatWindowController.WrapText( logicalLine, maxWidth, Measure)) { lines.Add(new UiText.Line(wrapped, color)); } } } return lines; } private static Vector4 ResolveColor( UiText target, ItemAppraisalFontStyle style) { int index = (int)style; return index >= 0 && index < target.FontColorPalette.Count ? target.FontColorPalette[index] : target.DefaultColor; } }