From dc442e096b85c1bde0f824f6f8aa4036076faf97 Mon Sep 17 00:00:00 2001 From: UnilTan <74077609+UnilTan@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:12:16 +0500 Subject: [PATCH] Add alignment support for RichTextLabel Added Align to RichTextLabel and passed it into RichTextEntry.Draw(). Updated RichTextEntry to calculate line widths and apply horizontal offsets so wrapped rich text can be centered or right-aligned correctly. --- .../UserInterface/Controls/RichTextLabel.cs | 5 +- Robust.Client/UserInterface/RichTextEntry.cs | 91 +++++++++++++++++-- 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/Robust.Client/UserInterface/Controls/RichTextLabel.cs b/Robust.Client/UserInterface/Controls/RichTextLabel.cs index 75594846429..0f6646734e9 100644 --- a/Robust.Client/UserInterface/Controls/RichTextLabel.cs +++ b/Robust.Client/UserInterface/Controls/RichTextLabel.cs @@ -21,6 +21,9 @@ public class RichTextLabel : Control private float _lineHeightScale = 1; private bool _lineHeightOverride; + [ViewVariables] + public Label.AlignMode Align { get; set; } = Label.AlignMode.Left; + [ViewVariables(VVAccess.ReadWrite)] public float LineHeightScale { @@ -105,7 +108,7 @@ protected override Vector2 MeasureOverride(Vector2 availableSize) protected internal override void Draw(DrawingHandleScreen handle) { base.Draw(handle); - _entry?.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale); + _entry?.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale, Align); } [Pure] diff --git a/Robust.Client/UserInterface/RichTextEntry.cs b/Robust.Client/UserInterface/RichTextEntry.cs index 0e8898ef075..b94d74f41d6 100644 --- a/Robust.Client/UserInterface/RichTextEntry.cs +++ b/Robust.Client/UserInterface/RichTextEntry.cs @@ -119,7 +119,7 @@ public RichTextEntry Update(MarkupTagManager tagManager, Font defaultFont, float foreach (var node in Message) { nodeIndex++; - var text = ProcessNode(tagManager, node, context); + var text = ProcessNode(tagManager, node, context, _tagsAllowed); if (!context.Font.TryPeek(out var font)) font = defaultFont; @@ -208,22 +208,28 @@ public readonly void Draw( float verticalOffset, MarkupDrawingContext context, float uiScale, - float lineHeightScale = 1) + float lineHeightScale = 1, + Robust.Client.UserInterface.Controls.Label.AlignMode align = Robust.Client.UserInterface.Controls.Label.AlignMode.Left) { context.Clear(); context.Color.Push(_defaultColor); context.Font.Push(defaultFont); + var lineWidths = align == Robust.Client.UserInterface.Controls.Label.AlignMode.Left + ? null + : CalculateLineWidths(in this, tagManager, defaultFont, uiScale); + var globalBreakCounter = 0; var lineBreakIndex = 0; - var baseLine = drawBox.TopLeft + new Vector2(0, defaultFont.GetAscent(uiScale) + verticalOffset); + var currentLine = 0; + var baseLine = drawBox.TopLeft + new Vector2(GetAlignedOffset(currentLine), defaultFont.GetAscent(uiScale) + verticalOffset); var controlYAdvance = 0f; var nodeIndex = -1; foreach (var node in Message) { nodeIndex++; - var text = ProcessNode(tagManager, node, context); + var text = ProcessNode(tagManager, node, context, _tagsAllowed); if (!context.Color.TryPeek(out var color) || !context.Font.TryPeek(out var font)) { color = _defaultColor; @@ -235,7 +241,8 @@ public readonly void Draw( if (lineBreakIndex < LineBreaks.Count && LineBreaks[lineBreakIndex] == globalBreakCounter) { - baseLine = new Vector2(drawBox.Left, baseLine.Y + GetLineHeight(font, uiScale, lineHeightScale) + controlYAdvance); + currentLine += 1; + baseLine = new Vector2(drawBox.Left + GetAlignedOffset(currentLine), baseLine.Y + GetLineHeight(font, uiScale, lineHeightScale) + controlYAdvance); controlYAdvance = 0; lineBreakIndex += 1; } @@ -260,16 +267,86 @@ public readonly void Draw( controlYAdvance = Math.Max(0f, (control.DesiredPixelSize.Y - GetLineHeight(font, uiScale, lineHeightScale)) * invertedScale); baseLine += new Vector2(advanceX, 0); } + + float GetAlignedOffset(int lineIndex) + { + if (lineWidths == null || lineIndex < 0 || lineIndex >= lineWidths.Count) + return 0f; + + var remainingWidth = drawBox.Width - lineWidths[lineIndex]; + if (remainingWidth <= 0) + return 0f; + + return align switch + { + Robust.Client.UserInterface.Controls.Label.AlignMode.Right => remainingWidth, + Robust.Client.UserInterface.Controls.Label.AlignMode.Center or Robust.Client.UserInterface.Controls.Label.AlignMode.Fill => remainingWidth / 2f, + _ => 0f, + }; + } + } + + private static List CalculateLineWidths(in RichTextEntry entry, MarkupTagManager tagManager, Font defaultFont, float uiScale) + { + var widths = new List { 0 }; + var context = new MarkupDrawingContext(); + context.Color.Push(entry._defaultColor); + context.Font.Push(defaultFont); + + var globalBreakCounter = 0; + var lineBreakIndex = 0; + var currentLine = 0; + var nodeIndex = -1; + + foreach (var node in entry.Message) + { + nodeIndex++; + var text = ProcessNode(tagManager, node, context, entry._tagsAllowed); + if (!context.Font.TryPeek(out var font)) + font = defaultFont; + + foreach (var rune in text.EnumerateRunes()) + { + if (lineBreakIndex < entry.LineBreaks.Count && + entry.LineBreaks[lineBreakIndex] == globalBreakCounter) + { + currentLine += 1; + widths.Add(0); + lineBreakIndex += 1; + } + + if (font.TryGetCharMetrics(rune, uiScale, out var metrics)) + widths[currentLine] += metrics.Advance; + + globalBreakCounter += 1; + } + + if (entry.Controls == null || !entry.Controls.TryGetValue(nodeIndex, out var control)) + continue; + + if (lineBreakIndex < entry.LineBreaks.Count && + entry.LineBreaks[lineBreakIndex] == globalBreakCounter) + { + currentLine += 1; + widths.Add(0); + lineBreakIndex += 1; + } + + control.Measure(new Vector2(entry.Width, entry.Height)); + widths[currentLine] += control.DesiredPixelSize.X; + } + + return widths; } - private readonly string ProcessNode(MarkupTagManager tagManager, MarkupNode node, MarkupDrawingContext context) + private static string ProcessNode(MarkupTagManager tagManager, MarkupNode node, MarkupDrawingContext context, Type[]? tagsAllowed) { // If a nodes name is null it's a text node. if (node.Name == null) return node.Value.StringValue ?? ""; //Skip the node if there is no markup tag for it. - if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var tag)) + if (!tagManager.TryGetMarkupTagHandler(node.Name, tagsAllowed, out var tag)) return ""; if (!node.Closing)