From 4371067120c3fdb530ca970b85b465bdce6c51f3 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:13:12 -0500 Subject: [PATCH 01/44] Shared/Utility: Define new FormattedMessage core types --- Robust.Shared/Utility/FormattedMessage.cs | 267 ++++++++++------------ 1 file changed, 123 insertions(+), 144 deletions(-) diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index b67ca6866aa..59358ac7502 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -8,186 +8,165 @@ namespace Robust.Shared.Utility { - /// - /// Represents a formatted message in the form of a list of "tags". - /// Does not do any concrete formatting, simply useful as an API surface. - /// - [PublicAPI] [Serializable, NetSerializable] - public sealed partial class FormattedMessage + public struct Section { - public TagList Tags => new(_tags); - private readonly List _tags; - - public FormattedMessage() - { - _tags = new List(); - } - - public FormattedMessage(int capacity) - { - _tags = new List(capacity); - } - - public static FormattedMessage FromMarkup(string markup) - { - var msg = new FormattedMessage(); - msg.AddMarkup(markup); - return msg; - } - - public static FormattedMessage FromMarkupPermissive(string markup) - { - var msg = new FormattedMessage(); - msg.AddMarkupPermissive(markup); - return msg; - } - - /// - /// Escape a string of text to be able to be formatted into markup. - /// - public static string EscapeText(string text) - { - return text.Replace("\\", "\\\\").Replace("[", "\\["); - } - - /// - /// Remove all markup, leaving only the basic text content behind. - /// - public static string RemoveMarkup(string text) - { - return FromMarkup(text).ToString(); - } + public FontStyle Style; + public FontSize Size; + public TextAlign Alignment; + public int Color; + public MetaFlags Meta; + public string Content; + } - /// - /// Create a new FormattedMessage by copying another one. - /// - /// The message to copy. - public FormattedMessage(FormattedMessage toCopy) - { - _tags = toCopy._tags.ShallowClone(); - } + [Flags] + public enum MetaFlags : byte + { + Default = 0, + Localized = 1, + // All other values are reserved. + } - public void AddText(string text) - { - _tags.Add(new TagText(text)); - } + [Flags] + public enum FontStyle : byte + { + // Single-font styles + Normal = 0b0000_0000, + Bold = 0b0000_0001, + Italic = 0b0000_0010, + Monospace = 0b0000_0100, + BoldItalic = Bold | Italic, - public void PushColor(Color color) - { - _tags.Add(new TagColor(color)); - } + // Escape value + Special = 0b1000_0000, - public void PushNewline() - { - AddText("\n"); - } + // The lower four bits are available for styles to specify. + Standard = 0b0100_0000 | Special, - public void Pop() - { - _tags.Add(new TagPop()); - } + // All values not otherwise specified are reserved. + } - public void AddMessage(FormattedMessage other) - { - _tags.AddRange(other.Tags); - } + [Flags] + public enum FontSize : ushort + { + // Format (Standard): 0bSNNN_NNNN_NNNN_NNNN + // S: Special flag. + // N (where S == 0): Font size. Unsigned. + // N (where S == 1): Special operation; see below. + + // Flag to indicate the TagFontSize is "special". + // All values not specified are reserved. + // General Format: 0b1PPP_AAAA_AAAA_AAAA + // P: Operation. + // A: Arguments. + Special = 0b1000_0000_0000_0000, + + // RELative Plus. + // Format: 0b1100_NNNN_NNNN_NNNN + // N: Addend to the previous font size. Unsigned. + RelPlus = 0b0100_0000_0000_0000 | Special, + + // RELative Minus. + // Format: 0b1010_NNNN_NNNN_NNNN + // N: Subtrahend to the previous font size. Unsigned. + RelMinus = 0b0010_0000_0000_0000 | Special, + + // Selects a font size from the stylesheet. + // Format: 0b1110_NNNN_NNNN_NNNN + // N: The identifier of the preset font size. + Standard = 0b0110_0000_0000_0000 | Special, + } - public void Clear() - { - _tags.Clear(); - } + public enum TextAlign : byte + { + // Format: 0bHHHH_VVVV + // H: Horizontal alignment + // V: Vertical alignment. + // All values not specified are reserved. + + // This seems dumb to point out, but ok + Default = Baseline | Left, + + // Vertical alignment + Baseline = 0x00, + Top = 0x01, + Bottom = 0x02, + Superscript = 0x03, + Subscript = 0x04, + + // Horizontal alignment + Left = 0x00, + Right = 0x10, + Center = 0x20, + Justify = 0x30, + } - /// The string without markup tags. + [PublicAPI] + [Serializable, NetSerializable] + public sealed record FormattedMessage(Section[] Sections) + { public override string ToString() { - var builder = new StringBuilder(); - foreach (var tag in _tags) - { - if (tag is not TagText text) - { - continue; - } - - builder.Append(text.Text); - } + var sb = new StringBuilder(); + foreach (var i in Sections) + sb.Append(i.Content); - return builder.ToString(); + return sb.ToString(); } - /// The string without filtering out markup tags. - public string ToMarkup() + public class Builder { - var builder = new StringBuilder(); - foreach (var tag in _tags) - { - builder.Append(tag); - } + private bool _dirty = false; + private int _idx = 0; + private StringBuilder _sb = new(); + private List
_work = new(); - return builder.ToString(); - } - - [Serializable, NetSerializable] - public abstract record Tag - { - } - - [Serializable, NetSerializable] - public sealed record TagText(string Text) : Tag - { - public override string ToString() + public void Clear() { - return Text; + _idx = 0; + _work = new(); + _sb = _sb.Clear(); } - } - [Serializable, NetSerializable] - public sealed record TagColor(Color Color) : Tag - { - public override string ToString() + public void AddText(string text) { - return $"[color={Color.ToHex()}]"; + _dirty = true; + _sb.Append(text); } - } - [Serializable, NetSerializable] - public sealed record TagPop : Tag - { - public static readonly TagPop Instance = new(); - - public override string ToString() + public void PushColor(Color color) { - return $"[/color]"; + flushWork(); + _idx++; + var last = _work[_work.Count - 1]; + last.Color = color.ToArgb(); + _work[_work.Count - 1] = last; } - } - - public readonly struct TagList : IReadOnlyList - { - private readonly List _tags; - public TagList(List tags) + public void PushNewline() { - _tags = tags; + _dirty = true; + _sb.Append('\n'); } - public List.Enumerator GetEnumerator() + public void Pop() { - return _tags.GetEnumerator(); + flushWork(); + _idx--; } - IEnumerator IEnumerable.GetEnumerator() + public void flushWork() { - return _tags.GetEnumerator(); - } + if (!_dirty) + return; - IEnumerator IEnumerable.GetEnumerator() - { - return _tags.GetEnumerator(); + var last = _work[_work.Count - 1]; + last.Content = _sb.ToString(); + _sb = _sb.Clear(); + _work.Add(_work[_idx]); } - public int Count => _tags.Count; - - public Tag this[int index] => _tags[index]; + public FormattedMessage Build() => new FormattedMessage(_work.ToArray()); } } } From 29eff23643cba228ab5a0bf1959ea9710dce4aa3 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:15:30 -0500 Subject: [PATCH 02/44] Shared/Utility: Move MarkupParser to a new namespace, update to new FormattedText --- .../Basic.cs} | 66 +++++++++++++------ 1 file changed, 46 insertions(+), 20 deletions(-) rename Robust.Shared/Utility/{FormattedMessage.MarkupParser.cs => Markup/Basic.cs} (56%) diff --git a/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs b/Robust.Shared/Utility/Markup/Basic.cs similarity index 56% rename from Robust.Shared/Utility/FormattedMessage.MarkupParser.cs rename to Robust.Shared/Utility/Markup/Basic.cs index 0ab2b7f74e8..8377e822f46 100644 --- a/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs +++ b/Robust.Shared/Utility/Markup/Basic.cs @@ -1,13 +1,22 @@ +using System; using System.Collections.Generic; using Pidgin; using Robust.Shared.Maths; +using static Robust.Shared.Utility.FormattedMessage; using static Pidgin.Parser; using static Pidgin.Parser; -namespace Robust.Shared.Utility +namespace Robust.Shared.Utility.Markup { - public partial class FormattedMessage + public class Basic { + internal record tag; + internal record tagText(string text) : tag; + internal record tagColor(Color color) : tag; + internal record tagPop() : tag; + + private List _tags = new(); + // wtf I love parser combinators now. private const char TagBegin = '['; private const char TagEnd = ']'; @@ -18,46 +27,45 @@ public partial class FormattedMessage Char(TagBegin), Char(TagEnd))); - private static readonly Parser ParseTagText = + private static readonly Parser ParseTagText = ParseEscapeSequence.Or(Token(c => c != TagBegin && c != '\\')) .AtLeastOnceString() - .Select(s => new TagText(s)); + .Select(s => new tagText(s)); - private static readonly Parser ParseTagColor = + private static readonly Parser ParseTagColor = String("color") .Then(Char('=')) .Then(Token(ValidColorNameContents).AtLeastOnceString() .Select(s => { if (Color.TryFromName(s, out var color)) - { - return new TagColor(color); - } - return new TagColor(Color.FromHex(s)); + return new tagColor(color); + + return new tagColor(Color.FromHex(s)); })); - private static readonly Parser ParseTagPop = + private static readonly Parser ParseTagPop = Char('/') .Then(String("color")) - .ThenReturn(TagPop.Instance); + .ThenReturn(new tagPop()); - private static readonly Parser ParseTagContents = - ParseTagColor.Cast().Or(ParseTagPop.Cast()); + private static readonly Parser ParseTagContents = + ParseTagColor.Cast().Or(ParseTagPop.Cast()); - private static readonly Parser ParseEnclosedTag = + private static readonly Parser ParseEnclosedTag = ParseTagContents.Between(Char(TagBegin), Char(TagEnd)); - private static readonly Parser ParseTagOrFallBack = + private static readonly Parser ParseTagOrFallBack = Try(ParseEnclosedTag) // If we couldn't parse a tag then parse the [ of the start of the tag // so the rest is recognized as text. - .Or(Char(TagBegin).ThenReturn(new TagText("["))); + .Or(Char(TagBegin).ThenReturn(new tagText("["))); - private static readonly Parser> Parse = - ParseTagText.Cast().Or(ParseEnclosedTag).Many(); + private static readonly Parser> Parse = + ParseTagText.Cast().Or(ParseEnclosedTag).Many(); - private static readonly Parser> ParsePermissive = - ParseTagText.Cast().Or(ParseTagOrFallBack).Many(); + private static readonly Parser> ParsePermissive = + ParseTagText.Cast().Or(ParseTagOrFallBack).Many(); public static bool ValidMarkup(string markup) { @@ -102,5 +110,23 @@ private static bool ValidColorNameContents(char c) return false; } + + + public FormattedMessage Render() + { + var b = new FormattedMessage.Builder(); + + foreach (var t in _tags) + { + switch (t) + { + case tagText txt: b.AddText(txt.text); break; + case tagColor col: b.PushColor(col.color); break; + case tagPop: b.Pop(); break; + } + } + + return b.Build(); + } } } From c69294bbe0a47d9ca5ccbe9f7ef29d106e5ceba5 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:17:22 -0500 Subject: [PATCH 03/44] Shared/Serialization: Temporary fix for FormattedMessageSerializer --- .../Implementations/FormattedMessageSerializer.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/FormattedMessageSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/FormattedMessageSerializer.cs index 70f11b49548..567c10d7bf9 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/FormattedMessageSerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/FormattedMessageSerializer.cs @@ -8,6 +8,7 @@ using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Serialization.TypeSerializers.Interfaces; using Robust.Shared.Utility; +using Robust.Shared.Utility.Markup; namespace Robust.Shared.Serialization.TypeSerializers.Implementations { @@ -18,14 +19,16 @@ public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null) { - return new DeserializedValue(FormattedMessage.FromMarkup(node.Value)); + var bParser = new Basic(); + bParser.AddMarkup(node.Value); + return new DeserializedValue(bParser.Render()); } public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context = null) { - return FormattedMessage.ValidMarkup(node.Value) + return Basic.ValidMarkup(node.Value) ? new ValidatedValueNode(node) : new ErrorNode(node, "Invalid markup in FormattedMessage."); } @@ -41,7 +44,8 @@ public DataNode Write(ISerializationManager serializationManager, FormattedMessa public FormattedMessage Copy(ISerializationManager serializationManager, FormattedMessage source, FormattedMessage target, bool skipHook, ISerializationContext? context = null) { - return new(source); + // based value types + return source; } } } From 7a03e5b8caac6b1f24f473e8e97d8dbfc3c2d20e Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:17:58 -0500 Subject: [PATCH 04/44] Scripting/ScriptInstanceShared: Move to new FormattedMessage.Builder --- Robust.Shared.Scripting/ScriptInstanceShared.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Robust.Shared.Scripting/ScriptInstanceShared.cs b/Robust.Shared.Scripting/ScriptInstanceShared.cs index f214024613a..8f281d621b8 100644 --- a/Robust.Shared.Scripting/ScriptInstanceShared.cs +++ b/Robust.Shared.Scripting/ScriptInstanceShared.cs @@ -77,7 +77,7 @@ static ScriptInstanceShared() "var x = 5 + 5; var y = (object) \"foobar\"; void Foo(object a) { } Foo(y); Foo(x)"; var script = await CSharpScript.RunAsync(code); - var msg = new FormattedMessage(); + var msg = new FormattedMessage.Builder(); // Even run the syntax highlighter! AddWithSyntaxHighlighting(script.Script, msg, code, new AdhocWorkspace()); }); @@ -101,7 +101,7 @@ public static bool HasReturnValue(Script script) return _getDiagnosticArguments(diag); } - public static void AddWithSyntaxHighlighting(Script script, FormattedMessage msg, string code, + public static void AddWithSyntaxHighlighting(Script script, FormattedMessage.Builder msg, string code, Workspace workspace) { var compilation = script.GetCompilation(); From 5b23a74d0decf0fd1c9435b8ffd85b78f3f8e342 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:31:09 -0500 Subject: [PATCH 05/44] Shared/Utility: Add a FormattedMessage loader to the .Builder --- Robust.Shared/Utility/FormattedMessage.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index 59358ac7502..ce10765ba1f 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -121,6 +121,12 @@ public class Builder private StringBuilder _sb = new(); private List
_work = new(); + public static Builder FromFormattedText(FormattedMessage orig) => new () + { + _idx = orig.Sections.Length - 1, + _work = new List
(orig.Sections), + }; + public void Clear() { _idx = 0; From cf1739cedf670ad8a6c4d6a93dbab57fa74c41b1 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Tue, 9 Nov 2021 13:21:59 -0500 Subject: [PATCH 06/44] Server/Scripting: Port SciptHost to FormattedMessage.Builder --- Robust.Server/Scripting/ScriptHost.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Robust.Server/Scripting/ScriptHost.cs b/Robust.Server/Scripting/ScriptHost.cs index 3057c437b85..feb90da11c2 100644 --- a/Robust.Server/Scripting/ScriptHost.cs +++ b/Robust.Server/Scripting/ScriptHost.cs @@ -184,12 +184,12 @@ private async void ReceiveScriptEval(MsgScriptEval message) newScript.Compile(); // Echo entered script. - var echoMessage = new FormattedMessage(); + var echoMessage = new FormattedMessage.Builder(); ScriptInstanceShared.AddWithSyntaxHighlighting(newScript, echoMessage, code, instance.HighlightWorkspace); - replyMessage.Echo = echoMessage; + replyMessage.Echo = echoMessage.Build(); - var msg = new FormattedMessage(); + var msg = new FormattedMessage.Builder(); try { @@ -215,7 +215,7 @@ private async void ReceiveScriptEval(MsgScriptEval message) PromptAutoImports(e.Diagnostics, code, msg, instance); - replyMessage.Response = msg; + replyMessage.Response = msg.Build(); _netManager.ServerSendMessage(replyMessage, message.MsgChannel); return; } @@ -240,7 +240,7 @@ private async void ReceiveScriptEval(MsgScriptEval message) msg.AddText(ScriptInstanceShared.SafeFormat(instance.State.ReturnValue)); } - replyMessage.Response = msg; + replyMessage.Response = msg.Build(); _netManager.ServerSendMessage(replyMessage, message.MsgChannel); } @@ -316,7 +316,7 @@ private async void ReceiveScriptCompletion(MsgScriptCompletion message) private void PromptAutoImports( IEnumerable diags, string code, - FormattedMessage output, + FormattedMessage.Builder output, ScriptInstance instance) { if (!ScriptInstanceShared.CalcAutoImports(_reflectionManager, diags, out var found)) From 5ba079fd0ddcd217f7fa5a8a8a22095a1372f6a1 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 18 Nov 2021 12:20:40 -0500 Subject: [PATCH 07/44] UserInterface/RichTextEntry: NOP out almost everything not gonna bother fixing it until more groundwork is laid --- Robust.Client/UserInterface/RichTextEntry.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Robust.Client/UserInterface/RichTextEntry.cs b/Robust.Client/UserInterface/RichTextEntry.cs index 44490c1934d..a825d77cdef 100644 --- a/Robust.Client/UserInterface/RichTextEntry.cs +++ b/Robust.Client/UserInterface/RichTextEntry.cs @@ -15,9 +15,6 @@ namespace Robust.Client.UserInterface /// internal struct RichTextEntry { - private static readonly FormattedMessage.TagColor TagBaseColor - = new(new Color(200, 200, 200)); - public readonly FormattedMessage Message; /// @@ -51,6 +48,7 @@ public RichTextEntry(FormattedMessage message) /// public void Update(Font font, float maxSizeX, float uiScale) { + #if false // This method is gonna suck due to complexity. // Bear with me here. // I am so deeply sorry for the person adding stuff to this in the future. @@ -206,6 +204,7 @@ public void Update(Font font, float maxSizeX, float uiScale) } Width = (int) maxUsedWidth; + #endif } public void Draw( @@ -213,11 +212,9 @@ public void Draw( Font font, UIBox2 drawBox, float verticalOffset, - // A stack for format tags. - // This stack contains the format tag to RETURN TO when popped off. - // So when a new color tag gets hit this stack gets the previous color pushed on. - Stack formatStack, float uiScale) + float uiScale) { + #if false // The tag currently doing color. var currentColorTag = TagBaseColor; @@ -267,6 +264,7 @@ public void Draw( } } } + #endif } [Pure] From ec66b71603c122194f4e454f9fbd2aadd91fc0d5 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 18 Nov 2021 12:20:40 -0500 Subject: [PATCH 08/44] Shared/Utility: Expand Utility.Extensions a bit strictly for pesonal reasons --- Robust.Shared/Utility/CollectionExtensions.cs | 2 +- Robust.Shared/Utility/FormattedMessage.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Robust.Shared/Utility/CollectionExtensions.cs b/Robust.Shared/Utility/CollectionExtensions.cs index e84d0a907a2..2cfa47def7f 100644 --- a/Robust.Shared/Utility/CollectionExtensions.cs +++ b/Robust.Shared/Utility/CollectionExtensions.cs @@ -6,7 +6,7 @@ namespace Robust.Shared.Utility { - public static class Extensions + public static partial class Extensions { public static IList Clone(this IList listToClone) where T : ICloneable { diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index ce10765ba1f..a3971c4c68d 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -101,6 +101,12 @@ public enum TextAlign : byte Justify = 0x30, } + public static partial class Extensions + { + public static TextAlign Vertical (this TextAlign value) => (TextAlign)((byte) value & 0x0F); + public static TextAlign Horizontal (this TextAlign value) => (TextAlign)((byte) value & 0xF0); + } + [PublicAPI] [Serializable, NetSerializable] public sealed record FormattedMessage(Section[] Sections) @@ -114,6 +120,8 @@ public override string ToString() return sb.ToString(); } + // are you a construction worker? + // cuz you buildin public class Builder { private bool _dirty = false; From 730628f06ba2d7716a1fab9ec0f23a369fca2865 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 18 Nov 2021 12:20:40 -0500 Subject: [PATCH 09/44] Client/UserInterface: Add the base TextLayout engine --- Robust.Client/UserInterface/TextLayout.cs | 419 ++++++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 Robust.Client/UserInterface/TextLayout.cs diff --git a/Robust.Client/UserInterface/TextLayout.cs b/Robust.Client/UserInterface/TextLayout.cs new file mode 100644 index 00000000000..e53333b8c53 --- /dev/null +++ b/Robust.Client/UserInterface/TextLayout.cs @@ -0,0 +1,419 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; +using System.Text.Unicode; +using JetBrains.Annotations; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Robust.Client.UserInterface +{ + using FontBundle = Dictionary; + + public static class TextLayout + { + /// + /// An Offset is a simplified instruction for rendering a text block. + /// + /// + /// + /// Pseudocode for rendering: + /// + /// (int x, int y) topLeft = (10, 20); + /// foreach (var r in Lines) + /// { + /// var section = Message.Sections[section]; + /// var font = style.FontBundle[section.Style]; + /// font.DrawAt( + /// text=section.Content.Substring(charOffs, length), + /// x=topLeft.x + r.x, + /// y=topLeft.y + r.y, + /// color=new Color(section.Color) + /// ) + /// } + /// + /// + /// + /// + /// The index of the backing store (usually a ) in the + /// container (usually a ) to which the Offset belongs. + /// + /// The byte offset in to the to render. + /// The number of bytes after to render. + /// The offset from the base position's x coordinate to render this chunk of text. + /// The offset from the base position's y coordinate to render this chunk of text. + public class Offset + { + public Offset() + { + } + + public Offset(Offset o) + { + section = o.section; + charOffs = o.charOffs; + length = o.length; + x = o.x; + y = o.y; + } + + public int section; + public int charOffs; + public int length; + public int x; + public int y; + } + + public class RichTextBlock + { + public Offset[][] Lines; + public int Height; + public int Width; + public FormattedMessage Message; + } + + public enum WordType : byte + { + Normal, + Space, + LineBreak, + } + + public sealed class Word : Offset + { + public Word() + { + } + + public Word(Word o) : base(o) + { + h = o.h; + w = o.w; + spw = o.spw; + wt = o.wt; + } + public int h; + public int w; + public int spw; + public WordType wt; + } + + public static RichTextBlock Layout( + ISectionable text, + int w, + FontBundle fonts, + float scale = 1.0f, + int lineSpacing = 0, + int wordSpacing = 0, + int runeSpacing = 0, + LayoutOptions options = LayoutOptions.Default + ) => Layout( + text, + Split(text, fonts, scale, wordSpacing, runeSpacing, options), + w, + fonts, + scale, + lineSpacing, wordSpacing, + options + ); + + // Actually produce the layout data. + // The algorithm is basically ripped from CSS Flexbox. + // + // 1. Add up all the space each word takes + // 2. Subtract that from the line width (w) + // 3. Save that as the free space (fs) + // 4. Add up each gap's priority value (Σpri) + // 5. Assign each gap a final priority (fp) of ((priMax - pri) / Σpri) + // 6. That space has (fp*fs) pixels. + public static RichTextBlock Layout( + ISectionable src, + ImmutableArray text, + int w, + FontBundle fonts, + float scale = 1.0f, + int lineSpacing = 0, + int wordSpacing = 0, + LayoutOptions options = LayoutOptions.Default + ) + { + var lw = new WorkQueue<( + List wds, + List gaps, + int lnrem, + int sptot, + int maxPri, + int tPri + )>(); + + var lastAlign = TextAlign.Left; + + // Calculate line boundaries + foreach (var wd in text) + { + var hz = src[wd.section].Alignment.Horizontal(); + (int gW, int adv) = TransitionWeights(lastAlign, hz); + lastAlign = hz; + + lw.Work.gaps.Add(gW+lw.Work.maxPri); + lw.Work.tPri += gW+lw.Work.maxPri; + lw.Work.maxPri += adv; + + if (lw.Work.lnrem < wd.w) + { + lw.Flush(); + lw.Work.lnrem = w; + lw.Work.maxPri = 1; + } + + lw.Work.sptot += wd.spw; + lw.Work.lnrem -= wd.w + wd.spw; + lw.Work.wds.Add(wd); + } + lw.Flush(true); + + int py=0; + foreach ((var ln, var gaps, var lnrem, var sptot, var maxPri, var tPri) in lw.Done) + { + int px=0; + int lh=0; + var spDist = new int[gaps.Count]; + for (int i = 0; i < gaps.Count; i++) + { + spDist[i] = (int) (((float) gaps[i] / (float) tPri) * (float) sptot); + } + + int prevasc=0, prevdesc=0; + for (int i = 0; i < ln.Count; i++) + { + var ss = src[ln[i].section]; + var sf = fonts[ss.Style, ]; + var asc = sf.GetAscent(scale); + var desc = sf.GetDescent(scale); + px += spDist[i] + ln[i].w; + ln[i].x = px; + lh = Math.Max(lh, ln[i].h); + ln[i].y = src[ln[i].section].Alignment.Vertical() switch { + TextAlign.Baseline => 0, + TextAlign.Bottom => -(desc - prevdesc), // Scoot it up by the descent + TextAlign.Top => (asc - prevasc), + TextAlign.Subscript => -ln[i].h / 8, // Technically these should be derived from the font data, + TextAlign.Superscript => ln[i].h / 4, // but I'm not gonna bother figuring out how to pull it from them. + _ => 0, + }; + prevasc = asc; + prevdesc = desc; + } + py += lineSpacing + lh; + } + } + + private static (int gapPri, int adv) TransitionWeights (TextAlign l, TextAlign r) + { + l = l.Horizontal(); + r = r.Horizontal(); + + // Technically these could be slimmed down, but it's as much to help explain the system + // as it is to implement it. + + // p (aka gapPri) is how high up the food chain each gap should be. + // _LOWER_ p means more (since we do first-come first-serve). + + // a (aka adv) is how much we increment the gapPri counter, meaning how much less important + // future alignment changes are. + + // Left alignment. + (int p, int a) la = (l, r) switch { + ( TextAlign.Left, TextAlign.Left) => (0, 0), // Left alignment doesn't care about inter-word spacing + ( _, TextAlign.Left) => (0, 0), // or anything that comes before it, + ( TextAlign.Left, _) => (1, 1), // only what comes after it. + ( _, _) => (0, 0) + }; + + // Right alignment + (int p, int a) ra = (l, r) switch { + ( TextAlign.Right, TextAlign.Right) => (0, 0), // Right alignment also does not care about inter-word spacing, + ( _, TextAlign.Right) => (1, 1), // but it does care what comes before it, + ( TextAlign.Right, _) => (0, 0), // but not after. + ( _, _) => (0, 0) + }; + + // Centering + (int p, int a) ca = (l, r) switch { + ( TextAlign.Center, TextAlign.Center) => (0, 0), // Centering still doesn't care about inter-word spacing, + ( _, TextAlign.Center) => (1, 0), // but it cares about both what comes before it, + ( TextAlign.Center, _) => (1, 1), // and what comes after it. + ( _, _) => (0, 0) + }; + + // Justifying + (int p, int a) ja = (l, r) switch { + (TextAlign.Justify, TextAlign.Justify) => (1, 0), // Justification cares about inter-word spacing. + ( _, TextAlign.Justify) => (0, 1), // And (sort of) what comes before it. + ( _, _) => (0, 0) + }; + + return new + ( + la.p + ra.p + ca.p + ja.p, + la.a + ra.a + ca.a + ja.a + ); + } + + public interface ISectionable + { + Section this[int i] { get; } + int Length { get; } + } + + // Split creates a list of words broken based on their boundaries. + // Users are encouraged to reuse this for as long as it accurately reflects + // the content they're trying to display. + public static ImmutableArray Split( + ISectionable text, + FontBundle fonts, + float scale, + int wordSpacing, + int runeSpacing, + LayoutOptions options = LayoutOptions.Default + ) + { + var nofb = options.HasFlag(LayoutOptions.NoFallback); + + var s=0; + var lsbo=0; + var sbo=0; + var wq = new WorkQueue( + w => + { + var len = lsbo-sbo; + lsbo = sbo; + sbo = 0; + return new(w) { length=len }; + }, + () => new Word() { section=s, charOffs=sbo }, + w => w.length > 0 + ); + + for (s = 0; s < text.Length; s++) + { + var sec = text[s]; + sbo = 0; + var fnt = fonts[sec.Style, sec.Size]; + + foreach (var r in sec.Content.EnumerateRunes()) + { + if (r == (Rune) '\n') + { + wq.Flush(); + wq.Work.wt = WordType.LineBreak; + } + else if (Rune.IsSeparator(r)) + { + if (wq.Work.wt != WordType.Space) + { + wq.Work.w += wordSpacing; + wq.Flush(); + wq.Work.wt = WordType.Space; + } + } + else if (wq.Work.wt != WordType.Normal) + wq.Flush(); + + sbo += r.Utf16SequenceLength; + var cm = fnt.GetCharMetrics(r, scale, !nofb); + + if (!cm.HasValue) + { + if (nofb) + continue; + else + throw new Exception("unable to get character metrics"); + } + + // This may be less-than-optimal, since we're ignoring anything below the origin. + wq.Work.h = Math.Max(wq.Work.h, cm.Value.BearingY); + wq.Work.w += cm.Value.Advance; + if (wq.Work.wt == WordType.Normal) + wq.Work.spw = runeSpacing; + } + } + + wq.Flush(true); + + return wq.Done.ToImmutableArray(); + } + + [Flags] + public enum LayoutOptions : byte + { + Default = 0b0000_0000, + + // Measure the actual height of runes to space lines. + UseRenderTop = 0b0000_0001, + + // NoFallback disables the use of the Fallback character. + NoFallback = 0b0000_0010, + } + + // WorkQueue is probably a misnomer. All it does is streamline a pattern I ended up using + // repeatedly where I'd have a list of something and a WIP, then I'd flush the WIP in to + // the list. + private class WorkQueue + where TIn : new() + { + // _blank creates a new T if _refresh says it needs to. + private Func _blank = () => new TIn(); + + private Func _check = _ => true; + + private Func _conv; + + public List Done = new(); + public TIn Work; + + public WorkQueue( + Func conv, + Func? blank = default, + Func? check = default + ) + { + _conv = conv; + + if (blank is not null) + _blank = blank; + + if (check is not null) + _check = check; + + Work = _blank.Invoke(); + } + + public void Flush(bool force = false) + { + if (_check.Invoke(Work) || force) + { + Done.Add(_conv(Work)); + Work = _blank.Invoke(); + } + } + } + + private class WorkQueue : WorkQueue + where T : new() + { + private static Func __conv = i => i; + public WorkQueue( + Func? conv = default, + Func? blank = default, + Func? check = default + ) : base(conv ?? __conv, blank, check) + { + } + } + } +} From 3f18ec3320687fcf0223f15f7595e7ac3bdf4bee Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 02:16:42 -0500 Subject: [PATCH 10/44] Client/Graphics: Add a Font Library manager --- Robust.Client/Graphics/FontLibrary.cs | 151 ++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 Robust.Client/Graphics/FontLibrary.cs diff --git a/Robust.Client/Graphics/FontLibrary.cs b/Robust.Client/Graphics/FontLibrary.cs new file mode 100644 index 00000000000..e793a2b6f94 --- /dev/null +++ b/Robust.Client/Graphics/FontLibrary.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Robust.Shared.Utility; +using Robust.Client.ResourceManagement; +namespace Robust.Client.Graphics; + +/// +/// Stores a single style (Bold, Italic, Monospace), or any combination thereof. +/// +record FontVariant (FontStyle Style, FontResource[] Resource) +{ + public Font ToFont(byte size) + { + if (Resource.Length == 1) + return new VectorFont(Resource[0], size); + + var fs = new Font[Resource.Length]; + for (var i = 0; i < Resource.Length; i++) + fs[i] = new VectorFont(Resource[i], size); + + return new StackedFont(fs); + } +}; + +/// +/// Manages font-based bookkeeping across a single stylesheet. +/// +interface IFontLibrary +{ + /// Associates a name to a set of font resources. + void AddFont(string name, params FontVariant[] variants); + + /// Sets a standard size which can be reused across the Font Library. + void SetStandardSize(ushort number, byte size); + + /// Sets a standard style which can be reused across the Font Library. + void SetStandardStyle(ushort number, string name, FontStyle style); + + /// + /// Returns a fancy handle in to the library. + /// The handle keeps track of relative changes to and . + /// + IFontLibrarian StartFont(string id, FontStyle fst, FontSize fsz); +} + +/// +/// Acts as a handle in to an . +/// +interface IFontLibrarian +{ + Font Current { get; } + Font Update(FontStyle fst, FontSize fsz); +} + +class FontLibrary : IFontLibrary +{ + private Dictionary _styles = new(); + private Dictionary _standardSt = new(); + private Dictionary _standardSz = new(); + + void IFontLibrary.AddFont(string name, params FontVariant[] variants) => + _styles[name] = variants; + + IFontLibrarian IFontLibrary.StartFont(string id, FontStyle fst, FontSize fsz) => + new FontLibrarian(this, id, fst, fsz); + + void IFontLibrary.SetStandardStyle(ushort number, string name, FontStyle style) => + _standardSt[(FontStyle) number | FontStyle.Standard] = (name, style); + + void IFontLibrary.SetStandardSize(ushort number, byte size) => + _standardSz[(FontSize) number | FontSize.Standard] = size; + + private FontVariant lookup(string id, FontStyle fst) + { + if (fst.HasFlag(FontStyle.Standard)) + (id, fst) = _standardSt[fst]; + + FontVariant? winner = default; + foreach (var vr in _styles[id]) + { + var winfst = winner?.Style ?? ((FontStyle) 0); + + // Since the "style" flags are a bitfield, we can just see which one has more bits. + // More bits == closer to the desired font style. Free fallback! + if (BitOperations.PopCount((ulong) (vr.Style & fst)) > BitOperations.PopCount((ulong) (winfst & fst))) + winner = vr; + } + + if (winner is null) + throw new Exception("no matching font style"); + + return winner; + } + + private byte lookupSz(FontSize sz) + { + if (sz.HasFlag(FontSize.RelMinus) || sz.HasFlag(FontSize.RelPlus)) + throw new Exception("can't look up a relative font through a library; get a Librarian first"); + + if (sz.HasFlag(FontSize.Standard)) + return _standardSz[sz]; + + return (byte) sz; + } + + class FontLibrarian : IFontLibrarian + { + public Font Current => _current; + private Font _current; + + private FontLibrary _lib; + private string _id; + private FontStyle _fst; + private FontSize _fsz; + + public FontLibrarian(FontLibrary lib, string id, FontStyle fst, FontSize fsz) + { + _id = id; + _fst = fst; + _fsz = fsz; + _lib = lib; + + // Actual font entry + var f = lib.lookup(id, fst); + + // Real size + var rsz = (byte) lib.lookupSz(fsz); + _current = f.ToFont(rsz); + } + + Font IFontLibrarian.Update(FontStyle fst, FontSize fsz) + { + var f = _lib.lookup(_id, fst); + + byte rsz = (byte) _fsz; + var msk = (byte) fsz & 0b0000_1111; + if (fsz.HasFlag(FontSize.Standard)) + rsz = _lib.lookupSz(fsz); + else if (fsz.HasFlag(FontSize.RelPlus)) + rsz = (byte) (((byte) _fsz) + msk); + else if (fsz.HasFlag(FontSize.RelMinus)) + rsz = (byte) (((byte) _fsz) - msk); + + _fsz = (FontSize) rsz; + _fst = fst; + + return _current = f.ToFont((byte) rsz); + } + } +} From 58ea2381ed4621c1b9a3615d006957549ddd9d71 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 03:41:57 -0500 Subject: [PATCH 11/44] Graphics/TextLayout: Finish up implementing the TextLayout engine --- Robust.Client/Graphics/FontLibrary.cs | 31 ++++++++++-- Robust.Client/UserInterface/TextLayout.cs | 59 ++++++++++------------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/Robust.Client/Graphics/FontLibrary.cs b/Robust.Client/Graphics/FontLibrary.cs index e793a2b6f94..1f5256d537c 100644 --- a/Robust.Client/Graphics/FontLibrary.cs +++ b/Robust.Client/Graphics/FontLibrary.cs @@ -8,7 +8,7 @@ namespace Robust.Client.Graphics; /// /// Stores a single style (Bold, Italic, Monospace), or any combination thereof. /// -record FontVariant (FontStyle Style, FontResource[] Resource) +public record FontVariant (FontStyle Style, FontResource[] Resource) { public Font ToFont(byte size) { @@ -23,11 +23,20 @@ public Font ToFont(byte size) } }; +public record FontClass +( + string Id, + FontStyle Style, + FontSize Size +); + /// /// Manages font-based bookkeeping across a single stylesheet. /// -interface IFontLibrary +public interface IFontLibrary { + FontClass Default { get; } + /// Associates a name to a set of font resources. void AddFont(string name, params FontVariant[] variants); @@ -42,19 +51,33 @@ interface IFontLibrary /// The handle keeps track of relative changes to and . /// IFontLibrarian StartFont(string id, FontStyle fst, FontSize fsz); + + IFontLibrarian StartFont(FontClass? fclass = default) => + StartFont( + (fclass ?? Default).Id, + (fclass ?? Default).Style, + (fclass ?? Default).Size + ); } /// /// Acts as a handle in to an . /// -interface IFontLibrarian +public interface IFontLibrarian { Font Current { get; } Font Update(FontStyle fst, FontSize fsz); } -class FontLibrary : IFontLibrary +public class FontLibrary : IFontLibrary { + public FontClass Default { get; set; } + + public FontLibrary(FontClass def) + { + Default = def; + } + private Dictionary _styles = new(); private Dictionary _standardSt = new(); private Dictionary _standardSz = new(); diff --git a/Robust.Client/UserInterface/TextLayout.cs b/Robust.Client/UserInterface/TextLayout.cs index e53333b8c53..c8534348f09 100644 --- a/Robust.Client/UserInterface/TextLayout.cs +++ b/Robust.Client/UserInterface/TextLayout.cs @@ -1,19 +1,13 @@ using System; +using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; using System.Text; -using System.Text.Unicode; -using JetBrains.Annotations; using Robust.Client.Graphics; -using Robust.Client.UserInterface.Controls; -using Robust.Shared.Log; -using Robust.Shared.Maths; using Robust.Shared.Utility; namespace Robust.Client.UserInterface { - using FontBundle = Dictionary; - public static class TextLayout { /// @@ -24,10 +18,11 @@ public static class TextLayout /// Pseudocode for rendering: /// /// (int x, int y) topLeft = (10, 20); - /// foreach (var r in Lines) + /// var libn = style.FontLib.StartFont(defaultFontID, defaultFontStyle, defaultFontSize); + /// foreach (var r in returnedWords) /// { /// var section = Message.Sections[section]; - /// var font = style.FontBundle[section.Style]; + /// var font = libn.Update(section.Style, section.Size); /// font.DrawAt( /// text=section.Content.Substring(charOffs, length), /// x=topLeft.x + r.x, @@ -68,14 +63,6 @@ public Offset(Offset o) public int y; } - public class RichTextBlock - { - public Offset[][] Lines; - public int Height; - public int Width; - public FormattedMessage Message; - } - public enum WordType : byte { Normal, @@ -102,22 +89,30 @@ public Word(Word o) : base(o) public WordType wt; } - public static RichTextBlock Layout( + public interface ISectionable + { + Section this[int i] { get; } + int Length { get; } + } + + public static ImmutableArray> Layout( ISectionable text, int w, - FontBundle fonts, + IFontLibrary fonts, float scale = 1.0f, int lineSpacing = 0, int wordSpacing = 0, int runeSpacing = 0, + FontClass? fclass = default, LayoutOptions options = LayoutOptions.Default ) => Layout( text, - Split(text, fonts, scale, wordSpacing, runeSpacing, options), + Split(text, fonts, scale, wordSpacing, runeSpacing, fclass, options), w, fonts, scale, lineSpacing, wordSpacing, + fclass, options ); @@ -130,14 +125,15 @@ public static RichTextBlock Layout( // 4. Add up each gap's priority value (Σpri) // 5. Assign each gap a final priority (fp) of ((priMax - pri) / Σpri) // 6. That space has (fp*fs) pixels. - public static RichTextBlock Layout( + public static ImmutableArray> Layout( ISectionable src, ImmutableArray text, int w, - FontBundle fonts, + IFontLibrary fonts, float scale = 1.0f, int lineSpacing = 0, int wordSpacing = 0, + FontClass? fclass = default, LayoutOptions options = LayoutOptions.Default ) { @@ -176,6 +172,7 @@ int tPri } lw.Flush(true); + var flib = fonts.StartFont(fclass); int py=0; foreach ((var ln, var gaps, var lnrem, var sptot, var maxPri, var tPri) in lw.Done) { @@ -183,15 +180,13 @@ int tPri int lh=0; var spDist = new int[gaps.Count]; for (int i = 0; i < gaps.Count; i++) - { spDist[i] = (int) (((float) gaps[i] / (float) tPri) * (float) sptot); - } int prevasc=0, prevdesc=0; for (int i = 0; i < ln.Count; i++) { var ss = src[ln[i].section]; - var sf = fonts[ss.Style, ]; + var sf = flib.Update(ss.Style, ss.Size); var asc = sf.GetAscent(scale); var desc = sf.GetDescent(scale); px += spDist[i] + ln[i].w; @@ -210,6 +205,8 @@ int tPri } py += lineSpacing + lh; } + + return lw.Done.Select(l => l.wds.ToImmutableArray()).ToImmutableArray(); } private static (int gapPri, int adv) TransitionWeights (TextAlign l, TextAlign r) @@ -264,21 +261,16 @@ private static (int gapPri, int adv) TransitionWeights (TextAlign l, TextAlign r ); } - public interface ISectionable - { - Section this[int i] { get; } - int Length { get; } - } - // Split creates a list of words broken based on their boundaries. // Users are encouraged to reuse this for as long as it accurately reflects // the content they're trying to display. public static ImmutableArray Split( ISectionable text, - FontBundle fonts, + IFontLibrary fonts, float scale, int wordSpacing, int runeSpacing, + FontClass? fclass, LayoutOptions options = LayoutOptions.Default ) { @@ -299,11 +291,12 @@ public static ImmutableArray Split( w => w.length > 0 ); + var flib = fonts.StartFont(fclass); for (s = 0; s < text.Length; s++) { var sec = text[s]; sbo = 0; - var fnt = fonts[sec.Style, sec.Size]; + var fnt = flib.Update(sec.Style, sec.Size); foreach (var r in sec.Content.EnumerateRunes()) { From e230c8533f1871c539bea90d305e267926132b37 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 03:41:57 -0500 Subject: [PATCH 12/44] Utility/FormattedMessage: Add yet another hack to keep the serializer in service --- Robust.Shared/Utility/FormattedMessage.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index a3971c4c68d..68458ecdd80 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -120,6 +120,20 @@ public override string ToString() return sb.ToString(); } + // I don't wanna fix the serializer yet. + public string ToMarkup() + { + var sb = new StringBuilder(); + foreach (var i in Sections) + { + sb.AppendFormat("[color=#{0:X}]", i.Color); + sb.Append(i.Content); + sb.Append("[/color]"); + } + + return sb.ToString(); + } + // are you a construction worker? // cuz you buildin public class Builder From 5d9b84512ba1fcd3f0b9afacf0d0b80792fb1dca Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:17:52 -0500 Subject: [PATCH 13/44] Commands/Debug: Use FormattedMessage.Builder --- Robust.Client/Console/Commands/Debug.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Robust.Client/Console/Commands/Debug.cs b/Robust.Client/Console/Commands/Debug.cs index 96704f997a6..d231d9e9284 100644 --- a/Robust.Client/Console/Commands/Debug.cs +++ b/Robust.Client/Console/Commands/Debug.cs @@ -560,12 +560,12 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) vBox.AddChild(tree); var rich = new RichTextLabel(); - var message = new FormattedMessage(); + var message = new FormattedMessage.Builder(); message.AddText("Foo\n"); message.PushColor(Color.Red); message.AddText("Bar"); message.Pop(); - rich.SetMessage(message); + rich.SetMessage(message.Build()); vBox.AddChild(rich); var itemList = new ItemList(); From 8547efa6903eb3d35db3a7c47bf00fe89cc89253 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:22:18 -0500 Subject: [PATCH 14/44] Console/Completions: Use FormattedMessage.Builder --- Robust.Client/Console/Completions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Robust.Client/Console/Completions.cs b/Robust.Client/Console/Completions.cs index 6bc400162e1..bc601c075e8 100644 --- a/Robust.Client/Console/Completions.cs +++ b/Robust.Client/Console/Completions.cs @@ -83,7 +83,7 @@ public Entry(LiteResult result) { MouseFilter = MouseFilterMode.Stop; Result = result; - var compl = new FormattedMessage(); + var compl = new FormattedMessage.Builder(); var dim = Color.FromHsl((0f, 0f, 0.8f, 1f)); // warning: ew ahead @@ -120,7 +120,7 @@ public Entry(LiteResult result) compl.PushColor(Color.LightSlateGray); compl.AddText(Result.InlineDescription); } - SetMessage(compl); + SetMessage(compl.Build()); } } } From 8f0ae95644fb0da026001a0370b25daaf22e310d Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:33:47 -0500 Subject: [PATCH 15/44] Utility/FormattedMessage: Add `AddMessage` methods --- Robust.Shared/Utility/FormattedMessage.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index 68458ecdd80..5bd86c54a38 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -171,6 +171,12 @@ public void PushColor(Color color) _work[_work.Count - 1] = last; } + public void AddMessage(FormattedMessage other) => + _work.AddRange(other.Sections); + + public void AddMessage(FormattedMessage.Builder other) => + _work.AddRange(other._work); + public void PushNewline() { _dirty = true; From 21b0058ded18032ebec78dd643dbed9c1b9cfd38 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:34:28 -0500 Subject: [PATCH 16/44] Console/ScriptConsole: Use FormattedMessage.Builder --- .../Console/ScriptClient.ScriptConsoleServer.cs | 4 ++-- Robust.Client/Console/ScriptConsoleClient.cs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Robust.Client/Console/ScriptClient.ScriptConsoleServer.cs b/Robust.Client/Console/ScriptClient.ScriptConsoleServer.cs index c5132561e08..73e0031501a 100644 --- a/Robust.Client/Console/ScriptClient.ScriptConsoleServer.cs +++ b/Robust.Client/Console/ScriptClient.ScriptConsoleServer.cs @@ -95,11 +95,11 @@ public void ReceiveResponse(MsgScriptResponse response) _linesEntered = 0; // Echo entered script. - var echoMessage = new FormattedMessage(); + var echoMessage = new FormattedMessage.Builder(); echoMessage.PushColor(Color.FromHex("#D4D4D4")); echoMessage.AddText("> "); echoMessage.AddMessage(response.Echo); - OutputPanel.AddMessage(echoMessage); + OutputPanel.AddMessage(echoMessage.Build()); OutputPanel.AddMessage(response.Response); diff --git a/Robust.Client/Console/ScriptConsoleClient.cs b/Robust.Client/Console/ScriptConsoleClient.cs index 5e6d5366114..62df23c189f 100644 --- a/Robust.Client/Console/ScriptConsoleClient.cs +++ b/Robust.Client/Console/ScriptConsoleClient.cs @@ -128,12 +128,12 @@ protected override async void Run() newScript.Compile(); // Echo entered script. - var echoMessage = new FormattedMessage(); + var echoMessage = new FormattedMessage.Builder(); echoMessage.PushColor(Color.FromHex("#D4D4D4")); echoMessage.AddText("> "); ScriptInstanceShared.AddWithSyntaxHighlighting(newScript, echoMessage, code, _highlightWorkspace); - OutputPanel.AddMessage(echoMessage); + OutputPanel.AddMessage(echoMessage.Build()); try { @@ -148,7 +148,7 @@ protected override async void Run() } catch (CompilationErrorException e) { - var msg = new FormattedMessage(); + var msg = new FormattedMessage.Builder(); msg.PushColor(Color.Crimson); @@ -158,7 +158,7 @@ protected override async void Run() msg.AddText("\n"); } - OutputPanel.AddMessage(msg); + OutputPanel.AddMessage(msg.Build()); OutputPanel.AddText(">"); PromptAutoImports(e.Diagnostics, code); @@ -167,16 +167,16 @@ protected override async void Run() if (_state.Exception != null) { - var msg = new FormattedMessage(); + var msg = new FormattedMessage.Builder(); msg.PushColor(Color.Crimson); msg.AddText(CSharpObjectFormatter.Instance.FormatException(_state.Exception)); - OutputPanel.AddMessage(msg); + OutputPanel.AddMessage(msg.Build()); } else if (ScriptInstanceShared.HasReturnValue(newScript)) { - var msg = new FormattedMessage(); + var msg = new FormattedMessage.Builder(); msg.AddText(ScriptInstanceShared.SafeFormat(_state.ReturnValue)); - OutputPanel.AddMessage(msg); + OutputPanel.AddMessage(msg.Build()); } OutputPanel.AddText(">"); From 7049ade6e522e115ee8fcf79c792980eb95f363d Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:36:41 -0500 Subject: [PATCH 17/44] Client/Log: Use FormattedMessage.Builder --- Robust.Client/Log/DebugConsoleLogHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Robust.Client/Log/DebugConsoleLogHandler.cs b/Robust.Client/Log/DebugConsoleLogHandler.cs index 060c47de778..0ed80a1b893 100644 --- a/Robust.Client/Log/DebugConsoleLogHandler.cs +++ b/Robust.Client/Log/DebugConsoleLogHandler.cs @@ -23,7 +23,7 @@ public void Log(string sawmillName, LogEvent message) if (sawmillName == "CON") return; - var formatted = new FormattedMessage(8); + var formatted = new FormattedMessage.Builder(); var robustLevel = message.Level.ToRobust(); formatted.PushColor(Color.DarkGray); formatted.AddText("["); @@ -38,7 +38,7 @@ public void Log(string sawmillName, LogEvent message) formatted.AddText("\n"); formatted.AddText(message.Exception.ToString()); } - Console.AddFormattedLine(formatted); + Console.AddFormattedLine(formatted.Build()); } private static Color LogLevelToColor(LogLevel level) From e5401c548d8484146d8eee5f8de635c263dcdf94 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:38:18 -0500 Subject: [PATCH 18/44] CustomControls/DebugConsole: Use FormattedMessage.Builder --- .../UserInterface/CustomControls/DebugConsole.xaml.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs index 85549c70e43..f1192848349 100644 --- a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs +++ b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Robust.Client.AutoGenerated; using Robust.Client.Console; -using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.ContentPack; @@ -132,11 +131,11 @@ private void OnHistoryChanged() public void AddLine(string text, Color color) { - var formatted = new FormattedMessage(3); + var formatted = new FormattedMessage.Builder(); formatted.PushColor(color); formatted.AddText(text); formatted.Pop(); - AddFormattedLine(formatted); + AddFormattedLine(formatted.Build()); } public void AddLine(string text) From 2b1a23bac0932c33322fd27a9ca789ce7a7945a6 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:40:33 -0500 Subject: [PATCH 19/44] Controls/OutputPanel: Use FormattedMessage.Builder, NOP `Draw` pending rewrite --- Robust.Client/UserInterface/Controls/OutputPanel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Robust.Client/UserInterface/Controls/OutputPanel.cs b/Robust.Client/UserInterface/Controls/OutputPanel.cs index 0fc5d4ddd3b..adce1c3ef01 100644 --- a/Robust.Client/UserInterface/Controls/OutputPanel.cs +++ b/Robust.Client/UserInterface/Controls/OutputPanel.cs @@ -74,9 +74,9 @@ public void RemoveEntry(Index index) public void AddText(string text) { - var msg = new FormattedMessage(); + var msg = new FormattedMessage.Builder(); msg.AddText(text); - AddMessage(msg); + AddMessage(msg.Build()); } public void AddMessage(FormattedMessage message) @@ -121,6 +121,7 @@ protected internal override void Draw(DrawingHandleScreen handle) var entryOffset = -_scrollBar.Value; +#if false // A stack for format tags. // This stack contains the format tag to RETURN TO when popped off. // So when a new color tag gets hit this stack gets the previous color pushed on. @@ -143,6 +144,7 @@ protected internal override void Draw(DrawingHandleScreen handle) entryOffset += entry.Height + font.GetLineSeparation(UIScale); } +#endif } protected internal override void MouseWheel(GUIMouseWheelEventArgs args) From 6de5ad49937de47dabed1210f099549c861b3f4d Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:42:13 -0500 Subject: [PATCH 20/44] Controls/RichTextLabel: Use FormattedMessage.Builder, NOP `Draw` pending rewrite --- Robust.Client/UserInterface/Controls/RichTextLabel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Robust.Client/UserInterface/Controls/RichTextLabel.cs b/Robust.Client/UserInterface/Controls/RichTextLabel.cs index cb05077b5f6..13adc9f031c 100644 --- a/Robust.Client/UserInterface/Controls/RichTextLabel.cs +++ b/Robust.Client/UserInterface/Controls/RichTextLabel.cs @@ -20,9 +20,9 @@ public void SetMessage(FormattedMessage message) public void SetMessage(string message) { - var msg = new FormattedMessage(); + var msg = new FormattedMessage.Builder(); msg.AddText(message); - SetMessage(msg); + SetMessage(msg.Build()); } protected override Vector2 MeasureOverride(Vector2 availableSize) @@ -47,7 +47,9 @@ protected internal override void Draw(DrawingHandleScreen handle) return; } +#if false _entry.Draw(handle, _getFont(), SizeBox, 0, new Stack(), UIScale); +#endif } [Pure] From 90f042aa368bdfabc0035a3ce644650b4b33b0f2 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 00:05:34 -0500 Subject: [PATCH 21/44] UnitTesting: Update FormattedMessage/Markup Tests They will NOT pass yet, but I don't care; it compiles. --- .../FormattedMessageSerializerTest.cs | 6 +- .../Shared/Utility/FormattedMessage_Test.cs | 69 ++++++++++--------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/FormattedMessageSerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/FormattedMessageSerializerTest.cs index 88f300b5037..48fd3ca7f33 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/FormattedMessageSerializerTest.cs +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/FormattedMessageSerializerTest.cs @@ -3,6 +3,7 @@ using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Utility; +using Robust.Shared.Utility.Markup; // ReSharper disable AccessToStaticMemberViaDerivedType @@ -17,8 +18,9 @@ public class FormattedMessageSerializerTest : SerializationTest [TestCase("[color=#FF0000FF]message[/color]")] public void SerializationTest(string text) { - var message = FormattedMessage.FromMarkup(text); - var node = Serialization.WriteValueAs(message); + var message = new Basic(); + message.AddMarkup(text); + var node = Serialization.WriteValueAs(message.Render()); Assert.That(node.Value, Is.EqualTo(text)); } diff --git a/Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs b/Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs index 1e2ee828a28..d796afc3709 100644 --- a/Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs +++ b/Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs @@ -1,43 +1,55 @@ -using System.Linq; using NUnit.Framework; using Robust.Shared.Maths; using Robust.Shared.Utility; +using Robust.Shared.Utility.Markup; namespace Robust.UnitTesting.Shared.Utility { [Parallelizable(ParallelScope.All)] [TestFixture] - [TestOf(typeof(FormattedMessage))] - public class FormattedMessage_Test + [TestOf(typeof(Basic))] + public class MarkupBasic_Test { [Test] public static void TestParseMarkup() { - var msg = FormattedMessage.FromMarkup("foo[color=#aabbcc]bar[/color]baz"); + var msg = new Basic(); + msg.AddMarkup("foo[color=#aabbcc]bar[/color]baz"); - Assert.That(msg.Tags, NUnit.Framework.Is.EquivalentTo(new FormattedMessage.Tag[] + Assert.That(msg.Render(), NUnit.Framework.Is.EquivalentTo(new FormattedMessage(new[] { - new FormattedMessage.TagText("foo"), - new FormattedMessage.TagColor(Color.FromHex("#aabbcc")), - new FormattedMessage.TagText("bar"), - FormattedMessage.TagPop.Instance, - new FormattedMessage.TagText("baz") - })); + new Section { + Content="foo" + }, + new Section { + Content="bar", + Color=0xAABBCC, + }, + new Section { + Content="baz" + } + }).Sections)); } [Test] public static void TestParseMarkupColorName() { - var msg = FormattedMessage.FromMarkup("foo[color=orange]bar[/color]baz"); + var msg = new Basic(); + msg.AddMarkup("foo[color=orange]bar[/color]baz"); - Assert.That(msg.Tags, NUnit.Framework.Is.EquivalentTo(new FormattedMessage.Tag[] + Assert.That(msg.Render(), NUnit.Framework.Is.EquivalentTo(new FormattedMessage(new[] { - new FormattedMessage.TagText("foo"), - new FormattedMessage.TagColor(Color.Orange), - new FormattedMessage.TagText("bar"), - FormattedMessage.TagPop.Instance, - new FormattedMessage.TagText("baz") - })); + new Section { + Content="foo" + }, + new Section { + Content="bar", + Color=Color.Orange.ToArgb(), + }, + new Section { + Content="baz" + } + }).Sections)); } [Test] @@ -46,30 +58,23 @@ public static void TestParseMarkupColorName() [TestCase("foo[stinky] bar")] public static void TestParsePermissiveMarkup(string text) { - var msg = FormattedMessage.FromMarkupPermissive(text); + var msg = new Basic(); + msg.AddMarkupPermissive(text); Assert.That( - string.Join("", msg.Tags.Cast().Select(p => p.Text)), + msg.Render().ToString(), NUnit.Framework.Is.EqualTo(text)); } - [Test] - [TestCase("Foo", ExpectedResult = "Foo")] - [TestCase("[color=red]Foo[/color]", ExpectedResult = "Foo")] - [TestCase("[color=red]Foo[/color]bar", ExpectedResult = "Foobar")] - public string TestRemoveMarkup(string test) - { - return FormattedMessage.RemoveMarkup(test); - } - [Test] [TestCase("Foo")] [TestCase("[color=#FF000000]Foo[/color]")] [TestCase("[color=#00FF00FF]Foo[/color]bar")] public static void TestToMarkup(string text) { - var message = FormattedMessage.FromMarkup(text); - Assert.That(message.ToMarkup(), NUnit.Framework.Is.EqualTo(text)); + var message = new Basic(); + message.AddMarkup(text); + Assert.That(message.Render().ToMarkup(), NUnit.Framework.Is.EqualTo(text)); } } } From 3881489e9bacd121da2096a213e9fd8bc5683497 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 00:25:34 -0500 Subject: [PATCH 22/44] Utility/FormattedMessage: Fix some off-by-one Builder bugs --- Robust.Shared/Utility/FormattedMessage.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index 5bd86c54a38..d75b52815fe 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -141,18 +141,22 @@ public class Builder private bool _dirty = false; private int _idx = 0; private StringBuilder _sb = new(); - private List
_work = new(); + private List
_work = new() { + new Section() + }; public static Builder FromFormattedText(FormattedMessage orig) => new () { - _idx = orig.Sections.Length - 1, + _idx = orig.Sections.Length < 0 ? orig.Sections.Length - 1 : 0, _work = new List
(orig.Sections), }; public void Clear() { _idx = 0; - _work = new(); + _work = new() { + new Section() + }; _sb = _sb.Clear(); } @@ -166,9 +170,10 @@ public void PushColor(Color color) { flushWork(); _idx++; - var last = _work[_work.Count - 1]; + var lidx = _work.Count > 0 ? _work.Count - 1 : 0; + var last = _work[lidx]; last.Color = color.ToArgb(); - _work[_work.Count - 1] = last; + _work[lidx] = last; } public void AddMessage(FormattedMessage other) => @@ -186,7 +191,7 @@ public void PushNewline() public void Pop() { flushWork(); - _idx--; + _idx = _idx < 0 ? _idx-1 : 0; } public void flushWork() @@ -194,10 +199,11 @@ public void flushWork() if (!_dirty) return; - var last = _work[_work.Count - 1]; + var lidx = _work.Count > 0 ? _work.Count - 1 : 0; + var last = _work[lidx]; last.Content = _sb.ToString(); _sb = _sb.Clear(); - _work.Add(_work[_idx]); + _work.Add(last); } public FormattedMessage Build() => new FormattedMessage(_work.ToArray()); From 9accc731ef9ccde31fad4cc958c471e6ab955f12 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 04:14:55 -0500 Subject: [PATCH 23/44] Utility/FormattedMessage: Continue cleanup, test compliance --- Robust.Shared/Utility/FormattedMessage.cs | 88 +++++++++++++++---- .../Shared/Utility/FormattedMessage_Test.cs | 34 +++---- 2 files changed, 83 insertions(+), 39 deletions(-) diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index d75b52815fe..f941a1bd652 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -1,5 +1,5 @@ using System; -using System.Collections; +using System.Linq; using System.Collections.Generic; using System.Text; using JetBrains.Annotations; @@ -9,14 +9,14 @@ namespace Robust.Shared.Utility { [Serializable, NetSerializable] - public struct Section + public record struct Section { public FontStyle Style; public FontSize Size; public TextAlign Alignment; public int Color; public MetaFlags Meta; - public string Content; + public string Content = string.Empty; } [Flags] @@ -123,12 +123,24 @@ public override string ToString() // I don't wanna fix the serializer yet. public string ToMarkup() { + #warning FormattedMessage.ToMarkup is still lossy. var sb = new StringBuilder(); foreach (var i in Sections) { - sb.AppendFormat("[color=#{0:X}]", i.Color); + if (i.Content.Length == 0) + continue; + + if (i.Color != default) + sb.AppendFormat("[color=#{0:X8}]", + // Bit twiddling to swap AARRGGBB to RRGGBBAA + ((i.Color << 8) & 0xFF_FF_FF_00) | // Drop alpha from the front + ((i.Color & 0xFF_00_00_00) >> 24) // Shuffle it to the back + ); + sb.Append(i.Content); - sb.Append("[/color]"); + + if (i.Color != default) + sb.Append("[/color]"); } return sb.ToString(); @@ -136,23 +148,40 @@ public string ToMarkup() // are you a construction worker? // cuz you buildin + [Obsolete("Construct FormattedMessage Sections manually.")] public class Builder { + // _dirty signals that _sb has content that needs flushing to _work private bool _dirty = false; + + // We fake a stack by keeping an index in to the work list. + // Since each Section contains all its styling info, we can "pop" the stack by + // using the (unchanged) Section before it. private int _idx = 0; private StringBuilder _sb = new(); + + // _work starts out with a dummy item because otherwise we break the assumption that + // _idx will always refer to *something* in _work. private List
_work = new() { new Section() }; public static Builder FromFormattedText(FormattedMessage orig) => new () { + // Again, we always need at least one _work item, so if the FormattedMessage + // is empty, we'll forge one. _idx = orig.Sections.Length < 0 ? orig.Sections.Length - 1 : 0, - _work = new List
(orig.Sections), + _work = new List
( + orig.Sections.Length == 0 ? + new [] { new Section() } + : orig.Sections + ), }; + // hmm what could this do public void Clear() { + _dirty = false; _idx = 0; _work = new() { new Section() @@ -160,53 +189,82 @@ public void Clear() _sb = _sb.Clear(); } + // Since we don't change any styling, we don't need to add a full Section. + // In these cases, we add it to the StringBuilder, and wait until styling IS changed, + // or we Render(). public void AddText(string text) { _dirty = true; _sb.Append(text); } + // PushColor changes the styling, so we need to submit any text we had waiting, then + // add a new empty Section with the new color. public void PushColor(Color color) { flushWork(); - _idx++; - var lidx = _work.Count > 0 ? _work.Count - 1 : 0; - var last = _work[lidx]; + + var last = _work[_idx]; last.Color = color.ToArgb(); - _work[lidx] = last; + _work.Add(last); + _idx = _work.Count - 1; } + // These next two are probably wildly bugged, since they'll include the other sections + // wholesale, and the entire fake-stack facade breaks down, since there's no way for the + // new stuff to inherit the previous style, and we don't know what parts of the style are + // actually set, and what parts are just default values. + + // TODO: move _idx? public void AddMessage(FormattedMessage other) => _work.AddRange(other.Sections); + // TODO: See above public void AddMessage(FormattedMessage.Builder other) => _work.AddRange(other._work); + // I wish I understood why this was needed... + // Did people not know you could AddText("\n")? public void PushNewline() { _dirty = true; _sb.Append('\n'); } + // Flush any text we've got for the current style, + // then roll back to the style before this one. public void Pop() { flushWork(); - _idx = _idx < 0 ? _idx-1 : 0; + // Go back one (or stay at the start) + _idx = (_idx > 0) ? (_idx - 1) : 0; } public void flushWork() { + // Nothing changed? Great. if (!_dirty) return; - var lidx = _work.Count > 0 ? _work.Count - 1 : 0; - var last = _work[lidx]; + // Get the last tag (for the style)... + var last = _work[_idx]; + // ...and set the content to the current buffer last.Content = _sb.ToString(); - _sb = _sb.Clear(); _work.Add(last); + + // Clean up + _sb = _sb.Clear(); + _dirty = false; } - public FormattedMessage Build() => new FormattedMessage(_work.ToArray()); + public FormattedMessage Build() + { + flushWork(); + return new FormattedMessage(_work + .GetRange(1, _work.Count - 1) // Drop the placeholder + .Where(e => e.Content.Length != 0) // and any blanks (which can happen from pushing colors and such) + .ToArray()); + } } } } diff --git a/Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs b/Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs index d796afc3709..f7bd0970567 100644 --- a/Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs +++ b/Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs @@ -16,19 +16,12 @@ public static void TestParseMarkup() var msg = new Basic(); msg.AddMarkup("foo[color=#aabbcc]bar[/color]baz"); - Assert.That(msg.Render(), NUnit.Framework.Is.EquivalentTo(new FormattedMessage(new[] + Assert.That(msg.Render().Sections, NUnit.Framework.Is.EquivalentTo(new[] { - new Section { - Content="foo" - }, - new Section { - Content="bar", - Color=0xAABBCC, - }, - new Section { - Content="baz" - } - }).Sections)); + new Section { Content="foo" }, + new Section { Content="bar", Color=unchecked ((int) 0xFFAABBCC) }, + new Section { Content="baz" } + })); } [Test] @@ -37,19 +30,12 @@ public static void TestParseMarkupColorName() var msg = new Basic(); msg.AddMarkup("foo[color=orange]bar[/color]baz"); - Assert.That(msg.Render(), NUnit.Framework.Is.EquivalentTo(new FormattedMessage(new[] + Assert.That(msg.Render().Sections, NUnit.Framework.Is.EquivalentTo(new[] { - new Section { - Content="foo" - }, - new Section { - Content="bar", - Color=Color.Orange.ToArgb(), - }, - new Section { - Content="baz" - } - }).Sections)); + new Section { Content="foo" }, + new Section { Content="bar", Color=Color.Orange.ToArgb() }, + new Section { Content="baz" } + })); } [Test] From 1d31ebd2592fafbfa66b51d2727a77dcb51f9650 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 04:57:00 -0500 Subject: [PATCH 24/44] Utility/FormattedMessage: Work around https://github.com/dotnet/roslyn/issues/57870 --- Robust.Shared/Utility/FormattedMessage.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index f941a1bd652..5401e9541fd 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -11,11 +11,11 @@ namespace Robust.Shared.Utility [Serializable, NetSerializable] public record struct Section { - public FontStyle Style; - public FontSize Size; - public TextAlign Alignment; - public int Color; - public MetaFlags Meta; + public FontStyle Style = default; + public FontSize Size = default; + public TextAlign Alignment = default; + public int Color = default; + public MetaFlags Meta = default; public string Content = string.Empty; } From b5dfd28c952eab4cb531f65d0f6c0756f96dea26 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 05:31:07 -0500 Subject: [PATCH 25/44] Utility/FormattedMessage: Move ISectionable from TextLayout, implement it for FormattedMessage --- Robust.Client/UserInterface/TextLayout.cs | 6 ------ Robust.Shared/Utility/FormattedMessage.cs | 12 +++++++++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Robust.Client/UserInterface/TextLayout.cs b/Robust.Client/UserInterface/TextLayout.cs index c8534348f09..cc429b85f70 100644 --- a/Robust.Client/UserInterface/TextLayout.cs +++ b/Robust.Client/UserInterface/TextLayout.cs @@ -89,12 +89,6 @@ public Word(Word o) : base(o) public WordType wt; } - public interface ISectionable - { - Section this[int i] { get; } - int Length { get; } - } - public static ImmutableArray> Layout( ISectionable text, int w, diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index 5401e9541fd..9007ee81b48 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -8,6 +8,12 @@ namespace Robust.Shared.Utility { + public interface ISectionable + { + Section this[int i] { get; } + int Length { get; } + } + [Serializable, NetSerializable] public record struct Section { @@ -109,7 +115,7 @@ public static partial class Extensions [PublicAPI] [Serializable, NetSerializable] - public sealed record FormattedMessage(Section[] Sections) + public sealed record FormattedMessage(Section[] Sections) : ISectionable { public override string ToString() { @@ -146,6 +152,10 @@ public string ToMarkup() return sb.ToString(); } + // Implements Robust.Client.UserInterface.TextLayout.ISectionable + public Section this[int i] { get => Sections[i]; } + public int Length { get => Sections.Length; } + // are you a construction worker? // cuz you buildin [Obsolete("Construct FormattedMessage Sections manually.")] From 5caa72fa3434fc17c4517c549f61b039b2a93d6f Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 07:25:47 -0500 Subject: [PATCH 26/44] UserInterface/TextLayout: Add a `postcreate` function to set up new `TIn`s Apparently Roslyn isn't big-brained enough to understand that a closure of type `Func` means that `var n = new(); return n;` requires `new()` to return a `T`. Ironically, it's significantly less cbt to add this than to convert that big tuple in `Layout` in to a class or struct of some sort (to initialize the `List<>`s). --- Robust.Client/UserInterface/TextLayout.cs | 24 +++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Robust.Client/UserInterface/TextLayout.cs b/Robust.Client/UserInterface/TextLayout.cs index cc429b85f70..725b18a96e7 100644 --- a/Robust.Client/UserInterface/TextLayout.cs +++ b/Robust.Client/UserInterface/TextLayout.cs @@ -138,7 +138,11 @@ public static ImmutableArray> Layout( int sptot, int maxPri, int tPri - )>(); + )>(postcreate: i => i with + { + wds = new List(), + gaps = new List() + }); var lastAlign = TextAlign.Left; @@ -355,18 +359,21 @@ private class WorkQueue { // _blank creates a new T if _refresh says it needs to. private Func _blank = () => new TIn(); + private Func? _postcr; private Func _check = _ => true; private Func _conv; + public List Done = new(); public TIn Work; public WorkQueue( Func conv, Func? blank = default, - Func? check = default + Func? check = default, + Func? postcreate = default ) { _conv = conv; @@ -377,7 +384,13 @@ public WorkQueue( if (check is not null) _check = check; + if (postcreate is not null) + _postcr = postcreate; + Work = _blank.Invoke(); + + if (_postcr is not null) + Work = _postcr.Invoke(Work); } public void Flush(bool force = false) @@ -386,6 +399,8 @@ public void Flush(bool force = false) { Done.Add(_conv(Work)); Work = _blank.Invoke(); + if (_postcr is not null) + Work = _postcr.Invoke(Work); } } } @@ -397,8 +412,9 @@ private class WorkQueue : WorkQueue public WorkQueue( Func? conv = default, Func? blank = default, - Func? check = default - ) : base(conv ?? __conv, blank, check) + Func? check = default, + Func? postcreate = default + ) : base(conv ?? __conv, blank, check, postcreate) { } } From e3b154efc48c7dc902a9faf6dad92114a00a695a Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 07:30:48 -0500 Subject: [PATCH 27/44] UserInterface/TextLayout: Throw if `Meta` isn't recognized TODO warning go brrr --- Robust.Client/UserInterface/TextLayout.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Robust.Client/UserInterface/TextLayout.cs b/Robust.Client/UserInterface/TextLayout.cs index 725b18a96e7..f5ce7e4b07c 100644 --- a/Robust.Client/UserInterface/TextLayout.cs +++ b/Robust.Client/UserInterface/TextLayout.cs @@ -293,6 +293,11 @@ public static ImmutableArray Split( for (s = 0; s < text.Length; s++) { var sec = text[s]; + + #warning Meta.Localized not yet implemented + if (sec.Meta != default) + throw new Exception("Text section with unknown or unimplemented Meta flag"); + sbo = 0; var fnt = flib.Update(sec.Style, sec.Size); From a3f9e072bcc8dff59e8bf8c45eed2fba84081317 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 07:32:52 -0500 Subject: [PATCH 28/44] Graphics/FontLibrary: Add a `DummyVariant` --- Robust.Client/Graphics/FontLibrary.cs | 9 +++++++-- Robust.Client/UserInterface/TextLayout.cs | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Robust.Client/Graphics/FontLibrary.cs b/Robust.Client/Graphics/FontLibrary.cs index 1f5256d537c..76d03e9a6fa 100644 --- a/Robust.Client/Graphics/FontLibrary.cs +++ b/Robust.Client/Graphics/FontLibrary.cs @@ -10,7 +10,7 @@ namespace Robust.Client.Graphics; ///
public record FontVariant (FontStyle Style, FontResource[] Resource) { - public Font ToFont(byte size) + public virtual Font ToFont(byte size) { if (Resource.Length == 1) return new VectorFont(Resource[0], size); @@ -23,6 +23,11 @@ public Font ToFont(byte size) } }; +internal record DummyVariant(FontStyle fs) : FontVariant(fs, new FontResource[0]) +{ + public override Font ToFont(byte size) => new DummyFont(); +}; + public record FontClass ( string Id, @@ -106,7 +111,7 @@ private FontVariant lookup(string id, FontStyle fst) // Since the "style" flags are a bitfield, we can just see which one has more bits. // More bits == closer to the desired font style. Free fallback! - if (BitOperations.PopCount((ulong) (vr.Style & fst)) > BitOperations.PopCount((ulong) (winfst & fst))) + if (BitOperations.PopCount((ulong) (vr.Style & fst)) >= BitOperations.PopCount((ulong) (winfst & fst))) winner = vr; } diff --git a/Robust.Client/UserInterface/TextLayout.cs b/Robust.Client/UserInterface/TextLayout.cs index f5ce7e4b07c..a3d1e81171d 100644 --- a/Robust.Client/UserInterface/TextLayout.cs +++ b/Robust.Client/UserInterface/TextLayout.cs @@ -327,6 +327,8 @@ public static ImmutableArray Split( { if (nofb) continue; + else if (fnt is DummyFont) + cm = new CharMetrics(); else throw new Exception("unable to get character metrics"); } From e8ed5416bb8431a1000d02c753efbcfb3609d825 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 07:36:57 -0500 Subject: [PATCH 29/44] UserInterface/UITheme: Move to FontLibraries --- Robust.Client/UserInterface/UITheme.cs | 28 ++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/Robust.Client/UserInterface/UITheme.cs b/Robust.Client/UserInterface/UITheme.cs index def3abe8616..77a0de65eb6 100644 --- a/Robust.Client/UserInterface/UITheme.cs +++ b/Robust.Client/UserInterface/UITheme.cs @@ -10,19 +10,39 @@ namespace Robust.Client.UserInterface /// public abstract class UITheme { - public abstract Font DefaultFont { get; } - public abstract Font LabelFont { get; } + public abstract IFontLibrary DefaultFontLibrary { get; } + public abstract IFontLibrary LabelFontLibrary { get; } + public Font DefaultFont { get => DefaultFontLibrary.StartFont().Current; } + public Font LabelFont { get => LabelFontLibrary.StartFont().Current; } public abstract StyleBox PanelPanel { get; } public abstract StyleBox ButtonStyle { get; } public abstract StyleBox LineEditBox { get; } } + public sealed class UIThemeDummy : UITheme { - public override Font DefaultFont { get; } = new DummyFont(); - public override Font LabelFont { get; } = new DummyFont(); + private static readonly FontClass _defaultFontClass = new FontClass ( Id: "dummy", Size: default, Style: default ); + public override IFontLibrary DefaultFontLibrary { get; } = new FontLibrary(_defaultFontClass); + public override IFontLibrary LabelFontLibrary { get; } = new FontLibrary(_defaultFontClass); public override StyleBox PanelPanel { get; } = new StyleBoxFlat(); public override StyleBox ButtonStyle { get; } = new StyleBoxFlat(); public override StyleBox LineEditBox { get; } = new StyleBoxFlat(); + + public UIThemeDummy() : base() + { + DefaultFontLibrary.AddFont("dummy", + new [] + { + new DummyVariant (default) + } + ); + LabelFontLibrary.AddFont("dummy", + new [] + { + new DummyVariant (default) + } + ); + } } } From 384cd4f4d0799b14fe074815ea83d7f14e1b567c Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 07:38:09 -0500 Subject: [PATCH 30/44] UserInterface/TextLayout: Move to an un-nested `ImmutableArray` --- Robust.Client/UserInterface/TextLayout.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Robust.Client/UserInterface/TextLayout.cs b/Robust.Client/UserInterface/TextLayout.cs index a3d1e81171d..26442fe593a 100644 --- a/Robust.Client/UserInterface/TextLayout.cs +++ b/Robust.Client/UserInterface/TextLayout.cs @@ -89,7 +89,7 @@ public Word(Word o) : base(o) public WordType wt; } - public static ImmutableArray> Layout( + public static ImmutableArray Layout( ISectionable text, int w, IFontLibrary fonts, @@ -119,7 +119,7 @@ public static ImmutableArray> Layout( // 4. Add up each gap's priority value (Σpri) // 5. Assign each gap a final priority (fp) of ((priMax - pri) / Σpri) // 6. That space has (fp*fs) pixels. - public static ImmutableArray> Layout( + public static ImmutableArray Layout( ISectionable src, ImmutableArray text, int w, @@ -204,7 +204,7 @@ int tPri py += lineSpacing + lh; } - return lw.Done.Select(l => l.wds.ToImmutableArray()).ToImmutableArray(); + return lw.Done.SelectMany(e => e.wds).ToImmutableArray(); } private static (int gapPri, int adv) TransitionWeights (TextAlign l, TextAlign r) From a6f0f45c9274085c8da1fe3df99b7815e6eb8bd4 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 07:46:10 -0500 Subject: [PATCH 31/44] UserInterface/RichTextEntry: Go ahead. Draw. --- .../UserInterface/Controls/OutputPanel.cs | 25 +- .../UserInterface/Controls/RichTextLabel.cs | 16 +- Robust.Client/UserInterface/RichTextEntry.cs | 251 +++--------------- 3 files changed, 48 insertions(+), 244 deletions(-) diff --git a/Robust.Client/UserInterface/Controls/OutputPanel.cs b/Robust.Client/UserInterface/Controls/OutputPanel.cs index adce1c3ef01..1d0f72a927d 100644 --- a/Robust.Client/UserInterface/Controls/OutputPanel.cs +++ b/Robust.Client/UserInterface/Controls/OutputPanel.cs @@ -62,7 +62,7 @@ public void RemoveEntry(Index index) var entry = _entries[index]; _entries.RemoveAt(index.GetOffset(_entries.Count)); - var font = _getFont(); + var font = _getFont().StartFont().Current; _totalContentHeight -= entry.Height + font.GetLineSeparation(UIScale); if (_entries.Count == 0) { @@ -86,7 +86,7 @@ public void AddMessage(FormattedMessage message) entry.Update(_getFont(), _getContentBox().Width, UIScale); _entries.Add(entry); - var font = _getFont(); + var font = _getFont().StartFont().Current; _totalContentHeight += entry.Height; if (_firstLine) { @@ -115,18 +115,12 @@ protected internal override void Draw(DrawingHandleScreen handle) base.Draw(handle); var style = _getStyleBox(); - var font = _getFont(); + var font = _getFont().StartFont().Current; style?.Draw(handle, PixelSizeBox); var contentBox = _getContentBox(); var entryOffset = -_scrollBar.Value; -#if false - // A stack for format tags. - // This stack contains the format tag to RETURN TO when popped off. - // So when a new color tag gets hit this stack gets the previous color pushed on. - var formatStack = new Stack(2); - foreach (var entry in _entries) { if (entryOffset + entry.Height < 0) @@ -140,11 +134,10 @@ protected internal override void Draw(DrawingHandleScreen handle) break; } - entry.Draw(handle, font, contentBox, entryOffset, formatStack, UIScale); + entry.Draw(handle, _getFont(), contentBox, entryOffset, UIScale); entryOffset += entry.Height + font.GetLineSeparation(UIScale); } -#endif } protected internal override void MouseWheel(GUIMouseWheelEventArgs args) @@ -184,7 +177,7 @@ private void _invalidateEntries() var entry = _entries[i]; entry.Update(font, sizeX, UIScale); _entries[i] = entry; - _totalContentHeight += entry.Height + font.GetLineSeparation(UIScale); + _totalContentHeight += entry.Height; } _scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight); @@ -195,14 +188,14 @@ private void _invalidateEntries() } [System.Diagnostics.Contracts.Pure] - private Font _getFont() + private IFontLibrary _getFont() { - if (TryGetStyleProperty("font", out var font)) + if (TryGetStyleProperty("font", out var font)) { return font; } - return UserInterfaceManager.ThemeDefaults.DefaultFont; + return UserInterfaceManager.ThemeDefaults.DefaultFontLibrary; } [System.Diagnostics.Contracts.Pure] @@ -220,7 +213,7 @@ private Font _getFont() [System.Diagnostics.Contracts.Pure] private int _getScrollSpeed() { - var font = _getFont(); + var font = _getFont().StartFont().Current; return font.GetLineHeight(UIScale) * 2; } diff --git a/Robust.Client/UserInterface/Controls/RichTextLabel.cs b/Robust.Client/UserInterface/Controls/RichTextLabel.cs index 13adc9f031c..13bdb5ee42a 100644 --- a/Robust.Client/UserInterface/Controls/RichTextLabel.cs +++ b/Robust.Client/UserInterface/Controls/RichTextLabel.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using JetBrains.Annotations; +using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -32,8 +31,7 @@ protected override Vector2 MeasureOverride(Vector2 availableSize) return Vector2.Zero; } - var font = _getFont(); - _entry.Update(font, availableSize.X * UIScale, UIScale); + _entry.Update(_getFont(), availableSize.X * UIScale, UIScale); return (_entry.Width / UIScale, _entry.Height / UIScale); } @@ -47,20 +45,18 @@ protected internal override void Draw(DrawingHandleScreen handle) return; } -#if false - _entry.Draw(handle, _getFont(), SizeBox, 0, new Stack(), UIScale); -#endif + _entry.Draw(handle, _getFont(), SizeBox, 0, UIScale); } [Pure] - private Font _getFont() + private IFontLibrary _getFont() { - if (TryGetStyleProperty("font", out var font)) + if (TryGetStyleProperty("font", out var font)) { return font; } - return UserInterfaceManager.ThemeDefaults.DefaultFont; + return UserInterfaceManager.ThemeDefaults.DefaultFontLibrary; } } } diff --git a/Robust.Client/UserInterface/RichTextEntry.cs b/Robust.Client/UserInterface/RichTextEntry.cs index a825d77cdef..26e88939e62 100644 --- a/Robust.Client/UserInterface/RichTextEntry.cs +++ b/Robust.Client/UserInterface/RichTextEntry.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Text; -using JetBrains.Annotations; +using System.Collections.Immutable; using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; -using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -27,250 +23,69 @@ internal struct RichTextEntry /// public int Width; - /// - /// The combined text indices in the message's text tags to put line breaks. - /// - public readonly List LineBreaks; - public RichTextEntry(FormattedMessage message) { Message = message; Height = 0; Width = 0; - LineBreaks = new List(); } + // Last maxSizeX, used to detect resizing. + private int _lmsx = 0; + // Layout data, which needs to be refreshed when resized. + private ImmutableArray _ld = default; + /// /// Recalculate line dimensions and where it has line breaks for word wrapping. /// /// The font being used for display. /// The maximum horizontal size of the container of this entry. /// - public void Update(Font font, float maxSizeX, float uiScale) + public void Update(IFontLibrary font, float maxSizeX, float uiScale) { - #if false - // This method is gonna suck due to complexity. - // Bear with me here. - // I am so deeply sorry for the person adding stuff to this in the future. - Height = font.GetHeight(uiScale); - LineBreaks.Clear(); - - var maxUsedWidth = 0f; - // Index we put into the LineBreaks list when a line break should occur. - var breakIndexCounter = 0; - // If the CURRENT processing word ends up too long, this is the index to put a line break. - (int index, float lineSize)? wordStartBreakIndex = null; - // Word size in pixels. - var wordSizePixels = 0; - // The horizontal position of the text cursor. - var posX = 0; - var lastRune = new Rune('A'); - // If a word is larger than maxSizeX, we split it. - // We need to keep track of some data to split it into two words. - (int breakIndex, int wordSizePixels)? forceSplitData = null; - // Go over every text tag. - // We treat multiple text tags as one continuous one. - // So changing color inside a single word doesn't create a word break boundary. - foreach (var tag in Message.Tags) - { - // For now we can ignore every entry that isn't a text tag because those are only color related. - // For now. - if (!(tag is FormattedMessage.TagText tagText)) - { - continue; - } - - var text = tagText.Text; - // And go over every character. - foreach (var rune in text.EnumerateRunes()) - { - breakIndexCounter += 1; - - if (IsWordBoundary(lastRune, rune) || rune == new Rune('\n')) - { - // Word boundary means we know where the word ends. - if (posX > maxSizeX && lastRune != new Rune(' ')) - { - DebugTools.Assert(wordStartBreakIndex.HasValue, - "wordStartBreakIndex can only be null if the word begins at a new line, in which case this branch shouldn't be reached as the word would be split due to being longer than a single line."); - // We ran into a word boundary and the word is too big to fit the previous line. - // So we insert the line break BEFORE the last word. - LineBreaks.Add(wordStartBreakIndex!.Value.index); - Height += font.GetLineHeight(uiScale); - maxUsedWidth = Math.Max(maxUsedWidth, wordStartBreakIndex.Value.lineSize); - posX = wordSizePixels; - } - - // Start a new word since we hit a word boundary. - //wordSize = 0; - wordSizePixels = 0; - wordStartBreakIndex = (breakIndexCounter, posX); - forceSplitData = null; - - // Just manually handle newlines. - if (rune == new Rune('\n')) - { - LineBreaks.Add(breakIndexCounter); - Height += font.GetLineHeight(uiScale); - maxUsedWidth = Math.Max(maxUsedWidth, posX); - posX = 0; - lastRune = rune; - wordStartBreakIndex = null; - continue; - } - } - - // Uh just skip unknown characters I guess. - if (!font.TryGetCharMetrics(rune, uiScale, out var metrics)) - { - lastRune = rune; - continue; - } - - // Increase word size and such with the current character. - var oldWordSizePixels = wordSizePixels; - wordSizePixels += metrics.Advance; - // TODO: Theoretically, does it make sense to break after the glyph's width instead of its advance? - // It might result in some more tight packing but I doubt it'd be noticeable. - // Also definitely even more complex to implement. - posX += metrics.Advance; - - if (posX > maxSizeX) - { - if (!forceSplitData.HasValue) - { - forceSplitData = (breakIndexCounter, oldWordSizePixels); - } - - // Oh hey we get to break a word that doesn't fit on a single line. - if (wordSizePixels > maxSizeX) - { - var (breakIndex, splitWordSize) = forceSplitData.Value; - if (splitWordSize == 0) - { - // Happens if there's literally not enough space for a single character so uh... - // Yeah just don't. - return; - } - - // Reset forceSplitData so that we can split again if necessary. - forceSplitData = null; - LineBreaks.Add(breakIndex); - Height += font.GetLineHeight(uiScale); - wordSizePixels -= splitWordSize; - wordStartBreakIndex = null; - maxUsedWidth = Math.Max(maxUsedWidth, maxSizeX); - posX = wordSizePixels; - } - } - - lastRune = rune; - } - } - - // This needs to happen because word wrapping doesn't get checked for the last word. - if (posX > maxSizeX) + if ((int) maxSizeX != _lmsx) { - if (!wordStartBreakIndex.HasValue) + _ld = TextLayout.Layout(Message, (int) maxSizeX, font, scale: uiScale); + Height = 0; + Width = 0; + foreach (var w in _ld) { - Logger.Error( - "Assert fail inside RichTextEntry.Update, " + - "wordStartBreakIndex is null on method end w/ word wrap required. " + - "Dumping relevant stuff. Send this to PJB."); - Logger.Error($"Message: {Message}"); - Logger.Error($"maxSizeX: {maxSizeX}"); - Logger.Error($"maxUsedWidth: {maxUsedWidth}"); - Logger.Error($"breakIndexCounter: {breakIndexCounter}"); - Logger.Error("wordStartBreakIndex: null (duh)"); - Logger.Error($"wordSizePixels: {wordSizePixels}"); - Logger.Error($"posX: {posX}"); - Logger.Error($"lastChar: {lastRune}"); - Logger.Error($"forceSplitData: {forceSplitData}"); - Logger.Error($"LineBreaks: {string.Join(", ", LineBreaks)}"); - - throw new Exception( - "wordStartBreakIndex can only be null if the word begins at a new line," + - "in which case this branch shouldn't be reached as" + - "the word would be split due to being longer than a single line."); + if (w.x + w.w > Width) Width = w.x + w.w; + if (w.y + w.h > Height) Height = w.y + w.h; } - - LineBreaks.Add(wordStartBreakIndex!.Value.index); - Height += font.GetLineHeight(uiScale); - maxUsedWidth = Math.Max(maxUsedWidth, wordStartBreakIndex.Value.lineSize); - } - else - { - maxUsedWidth = Math.Max(maxUsedWidth, posX); } - - Width = (int) maxUsedWidth; - #endif } public void Draw( DrawingHandleScreen handle, - Font font, + IFontLibrary font, UIBox2 drawBox, float verticalOffset, float uiScale) { - #if false - // The tag currently doing color. - var currentColorTag = TagBaseColor; - - var globalBreakCounter = 0; - var lineBreakIndex = 0; - var baseLine = drawBox.TopLeft + new Vector2(0, font.GetAscent(uiScale) + verticalOffset); - formatStack.Clear(); - foreach (var tag in Message.Tags) + var flib = font.StartFont(); + foreach (var wd in _ld) { - switch (tag) + var s = Message.Sections[wd.section]; + var baseLine = drawBox.TopLeft + new Vector2((float) wd.x, (float) wd.y); + foreach (var rune in s + .Content[wd.charOffs..(wd.charOffs+wd.length)] + .EnumerateRunes()) { - case FormattedMessage.TagColor tagColor: - formatStack.Push(currentColorTag); - currentColorTag = tagColor; - break; - case FormattedMessage.TagPop _: - var popped = formatStack.Pop(); - switch (popped) - { - case FormattedMessage.TagColor tagColor: - currentColorTag = tagColor; - break; - default: - throw new InvalidOperationException(); - } - - break; - case FormattedMessage.TagText tagText: - { - var text = tagText.Text; - foreach (var rune in text.EnumerateRunes()) - { - globalBreakCounter += 1; - - if (lineBreakIndex < LineBreaks.Count && - LineBreaks[lineBreakIndex] == globalBreakCounter) - { - baseLine = new Vector2(drawBox.Left, baseLine.Y + font.GetLineHeight(uiScale)); - lineBreakIndex += 1; + baseLine.X += flib.Current.DrawChar( + handle, + rune, + baseLine, + uiScale, + new Color { // Why Color.FromArgb isn't a thing is beyond me. + A=(float) ((s.Color & 0xFF_00_00_00) >> 24), + R=(float) ((s.Color & 0x00_FF_00_00) >> 16), + G=(float) ((s.Color & 0x00_00_FF_00) >> 8), + B=(float) (s.Color & 0x00_00_00_FF) } - - var advance = font.DrawChar(handle, rune, baseLine, uiScale, currentColorTag.Color); - baseLine += new Vector2(advance, 0); - } - - break; - } + ); } } - #endif - } - - [Pure] - private static bool IsWordBoundary(Rune a, Rune b) - { - return a == new Rune(' ') || b == new Rune(' ') || a == new Rune('-') || b == new Rune('-'); } } } From f47cf6effa6b78e699ff0f23c2186c21bc53876b Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Fri, 26 Nov 2021 08:39:22 -0500 Subject: [PATCH 32/44] Markup/Basic: Add extension & helpers for FormattedMessage.Builder --- Robust.Shared/Utility/Markup/Basic.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Robust.Shared/Utility/Markup/Basic.cs b/Robust.Shared/Utility/Markup/Basic.cs index 8377e822f46..70fc45420cd 100644 --- a/Robust.Shared/Utility/Markup/Basic.cs +++ b/Robust.Shared/Utility/Markup/Basic.cs @@ -1,8 +1,6 @@ -using System; using System.Collections.Generic; using Pidgin; using Robust.Shared.Maths; -using static Robust.Shared.Utility.FormattedMessage; using static Pidgin.Parser; using static Pidgin.Parser; @@ -112,7 +110,9 @@ private static bool ValidColorNameContents(char c) } - public FormattedMessage Render() + public FormattedMessage Render() => Build().Build(); + + public FormattedMessage.Builder Build() { var b = new FormattedMessage.Builder(); @@ -126,7 +126,21 @@ public FormattedMessage Render() } } - return b.Build(); + return b; + } + + public static FormattedMessage.Builder BuildMarkup(string text) + { + var nb = new Basic(); + nb.AddMarkup(text); + return nb.Build(); } + + public static FormattedMessage RenderMarkup(string text) => BuildMarkup(text).Build(); + } + + public static class FormattedMessageExtensions + { + public static void AddMarkup(this FormattedMessage.Builder bld, string text) => bld.AddMessage(Basic.BuildMarkup(text)); } } From d8238a61541d61a1aea4f8827eaaf4f329e2e911 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:37:16 -0500 Subject: [PATCH 33/44] Markup/Basic: Add `EscapeText` back in A forgotten casualty of the great Markup separation of 2021 --- Robust.Shared/Utility/Markup/Basic.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Robust.Shared/Utility/Markup/Basic.cs b/Robust.Shared/Utility/Markup/Basic.cs index 70fc45420cd..cfa70e5c9ed 100644 --- a/Robust.Shared/Utility/Markup/Basic.cs +++ b/Robust.Shared/Utility/Markup/Basic.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Pidgin; using Robust.Shared.Maths; @@ -137,10 +138,24 @@ public static FormattedMessage.Builder BuildMarkup(string text) } public static FormattedMessage RenderMarkup(string text) => BuildMarkup(text).Build(); + + /// + /// Escape a string of text to be able to be formatted into markup. + /// + public static string EscapeText(string text) + { + return text.Replace("\\", "\\\\").Replace("[", "\\["); + } } public static class FormattedMessageExtensions { public static void AddMarkup(this FormattedMessage.Builder bld, string text) => bld.AddMessage(Basic.BuildMarkup(text)); + + [Obsolete("Use Basic.EscapeText instead.")] + public static void EscapeText(this FormattedMessage _, string text) => Basic.EscapeText(text); + + [Obsolete("Use Basic.EscapeText instead.")] + public static void EscapeText(this FormattedMessage.Builder _, string text) => Basic.EscapeText(text); } } From b99acfc7829a6ed88b2cf2ec72f4ff945028ef6b Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:38:30 -0500 Subject: [PATCH 34/44] Graphics/FontLibrary: Clean up bit magic, ensure that at least one font is picked --- Robust.Client/Graphics/FontLibrary.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Robust.Client/Graphics/FontLibrary.cs b/Robust.Client/Graphics/FontLibrary.cs index 76d03e9a6fa..ece4635cae8 100644 --- a/Robust.Client/Graphics/FontLibrary.cs +++ b/Robust.Client/Graphics/FontLibrary.cs @@ -111,7 +111,13 @@ private FontVariant lookup(string id, FontStyle fst) // Since the "style" flags are a bitfield, we can just see which one has more bits. // More bits == closer to the desired font style. Free fallback! - if (BitOperations.PopCount((ulong) (vr.Style & fst)) >= BitOperations.PopCount((ulong) (winfst & fst))) + + // Variant's bit count + var vc = BitOperations.PopCount((ulong) (vr.Style & fst)); + // Winner's bit count + var wc = BitOperations.PopCount((ulong) (winfst & fst)); + + if (winner is null || vc > wc) winner = vr; } From 5b685b69c9c7cd9da3be8a976390c4d07f7ca1f7 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:39:23 -0500 Subject: [PATCH 35/44] Graphics/FontLibrary: Add diagnostics to the "no fonts" exception --- Robust.Client/Graphics/FontLibrary.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Client/Graphics/FontLibrary.cs b/Robust.Client/Graphics/FontLibrary.cs index ece4635cae8..ec8209cee61 100644 --- a/Robust.Client/Graphics/FontLibrary.cs +++ b/Robust.Client/Graphics/FontLibrary.cs @@ -122,7 +122,7 @@ private FontVariant lookup(string id, FontStyle fst) } if (winner is null) - throw new Exception("no matching font style"); + throw new Exception($"no matching font style ({id}, {fst})"); return winner; } From f8ab2b26252125dc730826557f794429ebd56d41 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:41:46 -0500 Subject: [PATCH 36/44] UserInterface/TextLayout: Scrap `Word`, return to `Offset` --- Robust.Client/UserInterface/RichTextEntry.cs | 2 +- Robust.Client/UserInterface/TextLayout.cs | 56 ++++++-------------- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/Robust.Client/UserInterface/RichTextEntry.cs b/Robust.Client/UserInterface/RichTextEntry.cs index 26e88939e62..3c01265e69a 100644 --- a/Robust.Client/UserInterface/RichTextEntry.cs +++ b/Robust.Client/UserInterface/RichTextEntry.cs @@ -33,7 +33,7 @@ public RichTextEntry(FormattedMessage message) // Last maxSizeX, used to detect resizing. private int _lmsx = 0; // Layout data, which needs to be refreshed when resized. - private ImmutableArray _ld = default; + private ImmutableArray _ld = default; /// /// Recalculate line dimensions and where it has line breaks for word wrapping. diff --git a/Robust.Client/UserInterface/TextLayout.cs b/Robust.Client/UserInterface/TextLayout.cs index 26442fe593a..9b8e4a501b6 100644 --- a/Robust.Client/UserInterface/TextLayout.cs +++ b/Robust.Client/UserInterface/TextLayout.cs @@ -41,26 +41,21 @@ public static class TextLayout /// The number of bytes after to render. /// The offset from the base position's x coordinate to render this chunk of text. /// The offset from the base position's y coordinate to render this chunk of text. - public class Offset + /// The width the word (i.e. the sum of all its Advance's). + /// The height of the tallest character's BearingY. + /// The width allocated to this word. + /// The detected word type. + public record struct Offset { - public Offset() - { - } - - public Offset(Offset o) - { - section = o.section; - charOffs = o.charOffs; - length = o.length; - x = o.x; - y = o.y; - } - public int section; public int charOffs; public int length; public int x; public int y; + public int h; + public int w; + public int spw; + public WordType wt; } public enum WordType : byte @@ -70,26 +65,7 @@ public enum WordType : byte LineBreak, } - public sealed class Word : Offset - { - public Word() - { - } - - public Word(Word o) : base(o) - { - h = o.h; - w = o.w; - spw = o.spw; - wt = o.wt; - } - public int h; - public int w; - public int spw; - public WordType wt; - } - - public static ImmutableArray Layout( + public static ImmutableArray Layout( ISectionable text, int w, IFontLibrary fonts, @@ -119,9 +95,9 @@ public static ImmutableArray Layout( // 4. Add up each gap's priority value (Σpri) // 5. Assign each gap a final priority (fp) of ((priMax - pri) / Σpri) // 6. That space has (fp*fs) pixels. - public static ImmutableArray Layout( + public static ImmutableArray Layout( ISectionable src, - ImmutableArray text, + ImmutableArray text, int w, IFontLibrary fonts, float scale = 1.0f, @@ -132,7 +108,7 @@ public static ImmutableArray Layout( ) { var lw = new WorkQueue<( - List wds, + List wds, List gaps, int lnrem, int sptot, @@ -140,7 +116,7 @@ public static ImmutableArray Layout( int tPri )>(postcreate: i => i with { - wds = new List(), + wds = new List(), gaps = new List() }); @@ -262,7 +238,7 @@ private static (int gapPri, int adv) TransitionWeights (TextAlign l, TextAlign r // Split creates a list of words broken based on their boundaries. // Users are encouraged to reuse this for as long as it accurately reflects // the content they're trying to display. - public static ImmutableArray Split( + public static ImmutableArray Split( ISectionable text, IFontLibrary fonts, float scale, @@ -277,7 +253,7 @@ public static ImmutableArray Split( var s=0; var lsbo=0; var sbo=0; - var wq = new WorkQueue( + var wq = new WorkQueue( w => { var len = lsbo-sbo; From bbbf03190053e1d7f7463d54a8e98fe80e4c1940 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:43:39 -0500 Subject: [PATCH 37/44] UserInterface/TextLayout: A whole bunch of hard-fought bugfixes --- Robust.Client/UserInterface/TextLayout.cs | 77 +++++++++++++++-------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/Robust.Client/UserInterface/TextLayout.cs b/Robust.Client/UserInterface/TextLayout.cs index 9b8e4a501b6..e17eee4419f 100644 --- a/Robust.Client/UserInterface/TextLayout.cs +++ b/Robust.Client/UserInterface/TextLayout.cs @@ -113,7 +113,8 @@ public static ImmutableArray Layout( int lnrem, int sptot, int maxPri, - int tPri + int tPri, + int lnh )>(postcreate: i => i with { wds = new List(), @@ -132,8 +133,9 @@ int tPri lw.Work.gaps.Add(gW+lw.Work.maxPri); lw.Work.tPri += gW+lw.Work.maxPri; lw.Work.maxPri += adv; + lw.Work.lnh = Math.Max(lw.Work.lnh, wd.h); - if (lw.Work.lnrem < wd.w) + if (lw.Work.lnrem < wd.w || wd.wt == WordType.LineBreak) { lw.Flush(); lw.Work.lnrem = w; @@ -147,37 +149,50 @@ int tPri lw.Flush(true); var flib = fonts.StartFont(fclass); - int py=0; - foreach ((var ln, var gaps, var lnrem, var sptot, var maxPri, var tPri) in lw.Done) + int py = flib.Current.GetAscent(scale); + foreach ((var ln, var gaps, var lnrem, var sptot, var maxPri, var tPri, var lnh) in lw.Done) { - int px=0; - int lh=0; + int px=0, maxlh=0; + var spDist = new int[gaps.Count]; for (int i = 0; i < gaps.Count; i++) spDist[i] = (int) (((float) gaps[i] / (float) tPri) * (float) sptot); - int prevasc=0, prevdesc=0; + int prevAsc=0, prevDesc=0; for (int i = 0; i < ln.Count; i++) { var ss = src[ln[i].section]; var sf = flib.Update(ss.Style, ss.Size); var asc = sf.GetAscent(scale); var desc = sf.GetDescent(scale); - px += spDist[i] + ln[i].w; - ln[i].x = px; - lh = Math.Max(lh, ln[i].h); - ln[i].y = src[ln[i].section].Alignment.Vertical() switch { - TextAlign.Baseline => 0, - TextAlign.Bottom => -(desc - prevdesc), // Scoot it up by the descent - TextAlign.Top => (asc - prevasc), - TextAlign.Subscript => -ln[i].h / 8, // Technically these should be derived from the font data, - TextAlign.Superscript => ln[i].h / 4, // but I'm not gonna bother figuring out how to pull it from them. - _ => 0, + maxlh = Math.Max(maxlh, sf.GetAscent(scale)); + + if (i - 1 > 0 && i - 1 < spDist.Length) + { + px += spDist[i - 1] / 2; + } + + ln[i] = ln[i] with { + x = px, + y = py + ss.Alignment.Vertical() switch { + TextAlign.Baseline => 0, + TextAlign.Bottom => -(desc - prevDesc), // Scoot it up by the descent + TextAlign.Top => (asc - prevAsc), + TextAlign.Subscript => -ln[i].h / 8, // Technically these should be derived from the font data, + TextAlign.Superscript => ln[i].h / 4, // but I'm not gonna bother figuring out how to pull it from them. + _ => 0, + } }; - prevasc = asc; - prevdesc = desc; + + if (i < spDist.Length) + { + px += spDist[i] / 2 + ln[i].w; + } + + prevAsc = asc; + prevDesc = desc; } - py += lineSpacing + lh; + py += options.HasFlag(LayoutOptions.UseRenderTop) ? lnh : (lineSpacing + maxlh); } return lw.Done.SelectMany(e => e.wds).ToImmutableArray(); @@ -256,13 +271,13 @@ public static ImmutableArray Split( var wq = new WorkQueue( w => { - var len = lsbo-sbo; + var len = sbo-lsbo; lsbo = sbo; - sbo = 0; - return new(w) { length=len }; + return w with { length=len }; }, - () => new Word() { section=s, charOffs=sbo }, - w => w.length > 0 + default, + default, + w => w with { section=s, charOffs=sbo } ); var flib = fonts.StartFont(fclass); @@ -274,8 +289,10 @@ public static ImmutableArray Split( if (sec.Meta != default) throw new Exception("Text section with unknown or unimplemented Meta flag"); + lsbo = 0; sbo = 0; var fnt = flib.Update(sec.Style, sec.Size); + wq.Reset(); foreach (var r in sec.Content.EnumerateRunes()) { @@ -315,9 +332,9 @@ public static ImmutableArray Split( if (wq.Work.wt == WordType.Normal) wq.Work.spw = runeSpacing; } + wq.Flush(true); } - wq.Flush(true); return wq.Done.ToImmutableArray(); } @@ -348,7 +365,6 @@ private class WorkQueue private Func _conv; - public List Done = new(); public TIn Work; @@ -376,6 +392,13 @@ public WorkQueue( Work = _postcr.Invoke(Work); } + public void Reset() + { + Work = _blank.Invoke(); + if (_postcr is not null) + Work = _postcr.Invoke(Work); + } + public void Flush(bool force = false) { if (_check.Invoke(Work) || force) From a1ffdf5f0c8313af7f24a7d1d6c3dd2aa886c06d Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:45:11 -0500 Subject: [PATCH 38/44] Utility/FormattedMessage: Add a static, empty FormattedMessage --- Robust.Shared/Utility/FormattedMessage.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index 9007ee81b48..b78868353b5 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -152,7 +152,8 @@ public string ToMarkup() return sb.ToString(); } - // Implements Robust.Client.UserInterface.TextLayout.ISectionable + public static readonly FormattedMessage Empty = new FormattedMessage(Array.Empty
()); + public Section this[int i] { get => Sections[i]; } public int Length { get => Sections.Length; } From 7f7876cbe88e7c99fcf9c03c49a75a2a4012a755 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:48:42 -0500 Subject: [PATCH 39/44] Utility/FormattedMessage: Fix. Bugs. --- Robust.Shared/Utility/FormattedMessage.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index b78868353b5..37051228713 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -216,6 +216,7 @@ public void PushColor(Color color) flushWork(); var last = _work[_idx]; + last.Content = string.Empty; last.Color = color.ToArgb(); _work.Add(last); _idx = _work.Count - 1; @@ -227,12 +228,20 @@ public void PushColor(Color color) // actually set, and what parts are just default values. // TODO: move _idx? - public void AddMessage(FormattedMessage other) => + public void AddMessage(FormattedMessage other) + { + flushWork(); _work.AddRange(other.Sections); + _idx = _work.Count-1; + } // TODO: See above - public void AddMessage(FormattedMessage.Builder other) => - _work.AddRange(other._work); + public void AddMessage(FormattedMessage.Builder other) + { + flushWork(); + AddMessage(other.Build()); + other.Clear(); + } // I wish I understood why this was needed... // Did people not know you could AddText("\n")? From 48951a3fd8cde2da302a5a295217944b9dadcb0a Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:50:21 -0500 Subject: [PATCH 40/44] UserInterface/RichTextEntry: Bug fixin' --- Robust.Client/UserInterface/RichTextEntry.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Robust.Client/UserInterface/RichTextEntry.cs b/Robust.Client/UserInterface/RichTextEntry.cs index 3c01265e69a..b9694514ee2 100644 --- a/Robust.Client/UserInterface/RichTextEntry.cs +++ b/Robust.Client/UserInterface/RichTextEntry.cs @@ -51,8 +51,9 @@ public void Update(IFontLibrary font, float maxSizeX, float uiScale) foreach (var w in _ld) { if (w.x + w.w > Width) Width = w.x + w.w; - if (w.y + w.h > Height) Height = w.y + w.h; + if (w.y + w.h > Height) Height = w.y; } + _lmsx = (int) maxSizeX; } } @@ -67,24 +68,27 @@ public void Draw( foreach (var wd in _ld) { var s = Message.Sections[wd.section]; - var baseLine = drawBox.TopLeft + new Vector2((float) wd.x, (float) wd.y); + var baseLine = drawBox.TopLeft + new Vector2((float) wd.x, verticalOffset + (float) wd.y); + foreach (var rune in s .Content[wd.charOffs..(wd.charOffs+wd.length)] .EnumerateRunes()) { + // TODO: Skip drawing when out of the drawBox baseLine.X += flib.Current.DrawChar( handle, rune, baseLine, uiScale, new Color { // Why Color.FromArgb isn't a thing is beyond me. - A=(float) ((s.Color & 0xFF_00_00_00) >> 24), - R=(float) ((s.Color & 0x00_FF_00_00) >> 16), - G=(float) ((s.Color & 0x00_00_FF_00) >> 8), - B=(float) (s.Color & 0x00_00_00_FF) + A=(float) ((s.Color & 0xFF_00_00_00) >> 24) / 255f, + R=(float) ((s.Color & 0x00_FF_00_00) >> 16) / 255f, + G=(float) ((s.Color & 0x00_00_FF_00) >> 8) / 255f, + B=(float) (s.Color & 0x00_00_00_FF) / 255f } ); } + } } } From 569ce41e8bded964e2d3f24f34a9813812d3e244 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:52:10 -0500 Subject: [PATCH 41/44] UserInterface: CSS teim --- .../UserInterface/Controls/ItemList.cs | 11 +++-- Robust.Client/UserInterface/Controls/Label.cs | 5 +- .../UserInterface/Controls/LineEdit.cs | 5 +- .../UserInterface/Controls/OutputPanel.cs | 46 +++++++++++++------ .../UserInterface/Controls/RichTextLabel.cs | 22 +++++++-- .../UserInterface/Controls/TabContainer.cs | 5 +- Robust.Client/UserInterface/Controls/Tree.cs | 5 +- Robust.Client/UserInterface/RichTextEntry.cs | 4 +- 8 files changed, 73 insertions(+), 30 deletions(-) diff --git a/Robust.Client/UserInterface/Controls/ItemList.cs b/Robust.Client/UserInterface/Controls/ItemList.cs index 41f074ba604..12ad858cbce 100644 --- a/Robust.Client/UserInterface/Controls/ItemList.cs +++ b/Robust.Client/UserInterface/Controls/ItemList.cs @@ -235,12 +235,17 @@ public Font ActualFont { get { - if (TryGetStyleProperty("font", out var font)) + TryGetStyleProperty("font", out var font); + if (TryGetStyleProperty("font-library", out var flib)) { - return font; + return flib.StartFont(font).Current; } - return UserInterfaceManager.ThemeDefaults.DefaultFont; + return UserInterfaceManager + .ThemeDefaults + .DefaultFontLibrary + .StartFont(font) + .Current; } } diff --git a/Robust.Client/UserInterface/Controls/Label.cs b/Robust.Client/UserInterface/Controls/Label.cs index 1c38520a0df..4c448288c1e 100644 --- a/Robust.Client/UserInterface/Controls/Label.cs +++ b/Robust.Client/UserInterface/Controls/Label.cs @@ -83,9 +83,10 @@ private Font ActualFont return FontOverride; } - if (TryGetStyleProperty(StylePropertyFont, out var font)) + TryGetStyleProperty(StylePropertyFont, out var font); + if (TryGetStyleProperty("font-library", out var flib)) { - return font; + return flib.StartFont(font).Current; } return UserInterfaceManager.ThemeDefaults.LabelFont; diff --git a/Robust.Client/UserInterface/Controls/LineEdit.cs b/Robust.Client/UserInterface/Controls/LineEdit.cs index b6dc5485643..3a970cfc066 100644 --- a/Robust.Client/UserInterface/Controls/LineEdit.cs +++ b/Robust.Client/UserInterface/Controls/LineEdit.cs @@ -667,9 +667,10 @@ protected internal override void KeyboardFocusExited() [Pure] private Font _getFont() { - if (TryGetStyleProperty("font", out var font)) + TryGetStyleProperty("font", out var font); + if (TryGetStyleProperty("font-library", out var flib)) { - return font; + return flib.StartFont(font).Current; } return UserInterfaceManager.ThemeDefaults.DefaultFont; diff --git a/Robust.Client/UserInterface/Controls/OutputPanel.cs b/Robust.Client/UserInterface/Controls/OutputPanel.cs index 1d0f72a927d..5013d3e5110 100644 --- a/Robust.Client/UserInterface/Controls/OutputPanel.cs +++ b/Robust.Client/UserInterface/Controls/OutputPanel.cs @@ -62,7 +62,7 @@ public void RemoveEntry(Index index) var entry = _entries[index]; _entries.RemoveAt(index.GetOffset(_entries.Count)); - var font = _getFont().StartFont().Current; + var font = _getFont(); _totalContentHeight -= entry.Height + font.GetLineSeparation(UIScale); if (_entries.Count == 0) { @@ -83,10 +83,10 @@ public void AddMessage(FormattedMessage message) { var entry = new RichTextEntry(message); - entry.Update(_getFont(), _getContentBox().Width, UIScale); + entry.Update(_getFontLib(), _getContentBox().Width, UIScale); _entries.Add(entry); - var font = _getFont().StartFont().Current; + var font = _getFont(); _totalContentHeight += entry.Height; if (_firstLine) { @@ -115,7 +115,8 @@ protected internal override void Draw(DrawingHandleScreen handle) base.Draw(handle); var style = _getStyleBox(); - var font = _getFont().StartFont().Current; + var flib = _getFontLib(); + var font = _getFont(); style?.Draw(handle, PixelSizeBox); var contentBox = _getContentBox(); @@ -134,7 +135,7 @@ protected internal override void Draw(DrawingHandleScreen handle) break; } - entry.Draw(handle, _getFont(), contentBox, entryOffset, UIScale); + entry.Draw(handle, flib, contentBox, entryOffset, UIScale, _getFontColor()); entryOffset += entry.Height + font.GetLineSeparation(UIScale); } @@ -170,7 +171,7 @@ protected override Vector2 MeasureOverride(Vector2 availableSize) private void _invalidateEntries() { _totalContentHeight = 0; - var font = _getFont(); + var font = _getFontLib(); var sizeX = _getContentBox().Width; for (var i = 0; i < _entries.Count; i++) { @@ -188,14 +189,32 @@ private void _invalidateEntries() } [System.Diagnostics.Contracts.Pure] - private IFontLibrary _getFont() + private IFontLibrary _getFontLib() { - if (TryGetStyleProperty("font", out var font)) - { - return font; - } + if (TryGetStyleProperty("font-library", out var flib)) + return flib; + + return UserInterfaceManager + .ThemeDefaults + .DefaultFontLibrary; + } + + [System.Diagnostics.Contracts.Pure] + private Font _getFont() + { + TryGetStyleProperty("font", out var fclass); + return _getFontLib().StartFont(fclass).Current; + } + + [System.Diagnostics.Contracts.Pure] + private Color _getFontColor() + { + if (TryGetStyleProperty("font-color", out var fc)) + return fc; - return UserInterfaceManager.ThemeDefaults.DefaultFontLibrary; + // From Robust.Client/UserInterface/RichTextEntry.cs#L19 + // at 33008a2bce0cc4755b18b12edfaf5b6f1f87fdd9 + return new Color(200, 200, 200); } [System.Diagnostics.Contracts.Pure] @@ -213,8 +232,7 @@ private IFontLibrary _getFont() [System.Diagnostics.Contracts.Pure] private int _getScrollSpeed() { - var font = _getFont().StartFont().Current; - return font.GetLineHeight(UIScale) * 2; + return _getFont().GetLineHeight(UIScale) * 2; } [System.Diagnostics.Contracts.Pure] diff --git a/Robust.Client/UserInterface/Controls/RichTextLabel.cs b/Robust.Client/UserInterface/Controls/RichTextLabel.cs index 13bdb5ee42a..453a517e727 100644 --- a/Robust.Client/UserInterface/Controls/RichTextLabel.cs +++ b/Robust.Client/UserInterface/Controls/RichTextLabel.cs @@ -45,18 +45,32 @@ protected internal override void Draw(DrawingHandleScreen handle) return; } - _entry.Draw(handle, _getFont(), SizeBox, 0, UIScale); + _entry.Draw(handle, _getFont(), SizeBox, 0, UIScale, _getFontColor()); } [Pure] private IFontLibrary _getFont() { - if (TryGetStyleProperty("font", out var font)) + TryGetStyleProperty("font", out var font); + if (TryGetStyleProperty("font-library", out var flib)) { - return font; + return flib; } - return UserInterfaceManager.ThemeDefaults.DefaultFontLibrary; + return UserInterfaceManager + .ThemeDefaults + .DefaultFontLibrary; + } + + [Pure] + private Color _getFontColor() + { + if (TryGetStyleProperty("font-color", out var fc)) + return fc; + + // From Robust.Client/UserInterface/RichTextEntry.cs#L19 + // at 33008a2bce0cc4755b18b12edfaf5b6f1f87fdd9 + return new Color(200, 200, 200); } } } diff --git a/Robust.Client/UserInterface/Controls/TabContainer.cs b/Robust.Client/UserInterface/Controls/TabContainer.cs index 3f5c5e08dc2..f3ee65c1a61 100644 --- a/Robust.Client/UserInterface/Controls/TabContainer.cs +++ b/Robust.Client/UserInterface/Controls/TabContainer.cs @@ -385,9 +385,10 @@ private Color _getTabFontColorInactive() [System.Diagnostics.Contracts.Pure] private Font _getFont() { - if (TryGetStyleProperty("font", out var font)) + TryGetStyleProperty("font", out var font); + if (TryGetStyleProperty("font-library", out var flib)) { - return font; + return flib.StartFont(font).Current; } return UserInterfaceManager.ThemeDefaults.DefaultFont; diff --git a/Robust.Client/UserInterface/Controls/Tree.cs b/Robust.Client/UserInterface/Controls/Tree.cs index c10166aca4c..52bc4769fae 100644 --- a/Robust.Client/UserInterface/Controls/Tree.cs +++ b/Robust.Client/UserInterface/Controls/Tree.cs @@ -284,9 +284,10 @@ protected override void Resized() private Font? _getFont() { - if (TryGetStyleProperty("font", out var font)) + TryGetStyleProperty("font", out var font); + if (TryGetStyleProperty("font-library", out var flib)) { - return font; + return flib.StartFont(font).Current; } return null; diff --git a/Robust.Client/UserInterface/RichTextEntry.cs b/Robust.Client/UserInterface/RichTextEntry.cs index b9694514ee2..bac87bfc0c2 100644 --- a/Robust.Client/UserInterface/RichTextEntry.cs +++ b/Robust.Client/UserInterface/RichTextEntry.cs @@ -62,7 +62,8 @@ public void Draw( IFontLibrary font, UIBox2 drawBox, float verticalOffset, - float uiScale) + float uiScale, + Color defColor) { var flib = font.StartFont(); foreach (var wd in _ld) @@ -80,6 +81,7 @@ public void Draw( rune, baseLine, uiScale, + s.Color == default ? defColor : new Color { // Why Color.FromArgb isn't a thing is beyond me. A=(float) ((s.Color & 0xFF_00_00_00) >> 24) / 255f, R=(float) ((s.Color & 0x00_FF_00_00) >> 16) / 255f, From b2f02cef36e279fa00c8ab3a1c8dae15383b3189 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Tue, 30 Nov 2021 14:36:58 -0500 Subject: [PATCH 42/44] Markup/Basic: Add an optional "default" style to use --- Robust.Shared/Utility/Markup/Basic.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Robust.Shared/Utility/Markup/Basic.cs b/Robust.Shared/Utility/Markup/Basic.cs index cfa70e5c9ed..2141a6a7abb 100644 --- a/Robust.Shared/Utility/Markup/Basic.cs +++ b/Robust.Shared/Utility/Markup/Basic.cs @@ -111,11 +111,17 @@ private static bool ValidColorNameContents(char c) } - public FormattedMessage Render() => Build().Build(); + public FormattedMessage Render(Section? defStyle = default) => Build(defStyle).Build(); - public FormattedMessage.Builder Build() + public FormattedMessage.Builder Build(Section? defStyle = default) { - var b = new FormattedMessage.Builder(); + FormattedMessage.Builder b; + if (defStyle != null) + b = FormattedMessage.Builder.FromFormattedMessage( + new FormattedMessage(new[] {defStyle.Value}) + ); + else + b = new FormattedMessage.Builder(); foreach (var t in _tags) { @@ -130,14 +136,14 @@ public FormattedMessage.Builder Build() return b; } - public static FormattedMessage.Builder BuildMarkup(string text) + public static FormattedMessage.Builder BuildMarkup(string text, Section? defStyle = default) { var nb = new Basic(); nb.AddMarkup(text); - return nb.Build(); + return nb.Build(defStyle); } - public static FormattedMessage RenderMarkup(string text) => BuildMarkup(text).Build(); + public static FormattedMessage RenderMarkup(string text, Section? defStyle = default) => BuildMarkup(text, defStyle).Build(); /// /// Escape a string of text to be able to be formatted into markup. From 0d07f089bc574f3f8065a5d05ffb89dbf9932c68 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Tue, 30 Nov 2021 14:37:33 -0500 Subject: [PATCH 43/44] Utility/FormattedMessage: I'm surprised I only made this mistake once. --- Robust.Shared/Utility/FormattedMessage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index 37051228713..f9613e78762 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -177,7 +177,7 @@ public class Builder new Section() }; - public static Builder FromFormattedText(FormattedMessage orig) => new () + public static Builder FromFormattedMessage(FormattedMessage orig) => new () { // Again, we always need at least one _work item, so if the FormattedMessage // is empty, we'll forge one. From c2ec30ff3afa47a60dc0599275674099fab8c293 Mon Sep 17 00:00:00 2001 From: Efruit <602406+Efruit@users.noreply.github.com> Date: Tue, 30 Nov 2021 14:37:55 -0500 Subject: [PATCH 44/44] Log/DebugConsoleLogHandler: work around lack of a default style --- Robust.Client/Log/DebugConsoleLogHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Robust.Client/Log/DebugConsoleLogHandler.cs b/Robust.Client/Log/DebugConsoleLogHandler.cs index 0ed80a1b893..9935af0ee18 100644 --- a/Robust.Client/Log/DebugConsoleLogHandler.cs +++ b/Robust.Client/Log/DebugConsoleLogHandler.cs @@ -32,7 +32,9 @@ public void Log(string sawmillName, LogEvent message) formatted.Pop(); formatted.AddText($"] {sawmillName}: "); formatted.Pop(); + formatted.PushColor(Color.LightGray); formatted.AddText(message.RenderMessage()); + formatted.Pop(); if (message.Exception != null) { formatted.AddText("\n");