From 20f1d944f11d1b7b7506a08971ebfcdef66f9f78 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 b57829ff918..87287d87371 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 { @@ -150,7 +153,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 e21c23833a6..fd780f20f2a 100644 --- a/Robust.Client/UserInterface/RichTextEntry.cs +++ b/Robust.Client/UserInterface/RichTextEntry.cs @@ -144,7 +144,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; @@ -233,15 +233,21 @@ 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 spaceRune = new Rune(' '); @@ -250,7 +256,7 @@ public readonly void Draw( 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; @@ -264,7 +270,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; @@ -301,16 +308,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)