Modify markup tag interfaces and fix some bugs (#5442)

* Modify markup tag interfaces

* Why are nullable structs like this.

* AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

* Avoid breaking changes

* Replace IMarkupTag with IMarkupTagHandler in engine

* Its a breaking change now I guess

* cleanup
This commit is contained in:
Leon Friedrich
2025-05-12 14:09:18 +10:00
committed by GitHub
parent 4d47cfa1a6
commit a393efc87a
14 changed files with 149 additions and 57 deletions
+2 -2
View File
@@ -35,7 +35,7 @@ END TEMPLATE-->
### Breaking changes
*None yet*
* `IMarkupTag` and related methods in `MarkupTagManager` have been obsoleted and should be replaced with the new `IMarkupTagHandler` interface. Various engine tags (e.g., `BoldTag`, `ColorTag`, etc) no longer implement the old interface.
### New features
@@ -43,7 +43,7 @@ END TEMPLATE-->
### Bugfixes
*None yet*
* OutputPanel and RichTextLabel now remove controls associated with rich text tags when the text is updated.
### Other
@@ -95,6 +95,12 @@ namespace Robust.Client.UserInterface.Controls
public void Clear()
{
_firstLine = true;
foreach (var entry in _entries)
{
entry.RemoveControls();
}
_entries.Clear();
_totalContentHeight = 0;
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
@@ -104,6 +110,7 @@ namespace Robust.Client.UserInterface.Controls
public void RemoveEntry(Index index)
{
var entry = _entries[index];
entry.RemoveControls();
_entries.RemoveAt(index.GetOffset(_entries.Count));
var font = _getFont();
@@ -189,6 +196,9 @@ namespace Robust.Client.UserInterface.Controls
if (entryOffset > contentBox.Height)
{
entry.HideControls();
// We know that every subsequent entry will also fail the test, but we also need to
// hide all the controls, so we cannot simply break out of the loop
continue;
}
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.Graphics;
@@ -15,8 +17,7 @@ namespace Robust.Client.UserInterface.Controls
{
[Dependency] private readonly MarkupTagManager _tagManager = default!;
private FormattedMessage? _message;
private RichTextEntry _entry;
private RichTextEntry? _entry;
private float _lineHeightScale = 1;
private bool _lineHeightOverride;
@@ -40,19 +41,26 @@ namespace Robust.Client.UserInterface.Controls
public string? Text
{
get => _message?.ToMarkup();
get => _entry?.Message.ToMarkup();
set
{
if (value == null)
{
_message?.Clear();
return;
}
SetMessage(FormattedMessage.FromMarkupPermissive(value));
Clear();
else
SetMessage(FormattedMessage.FromMarkupPermissive(value));
}
}
public void Clear()
{
_entry?.RemoveControls();
_entry = null;
InvalidateMeasure();
}
public IEnumerable<Control> Controls => _entry?.Controls?.Values ?? Enumerable.Empty<Control>();
public IReadOnlyList<MarkupNode> Nodes => _entry?.Message.Nodes ?? Array.Empty<MarkupNode>();
public RichTextLabel()
{
IoCManager.InjectDependencies(this);
@@ -61,8 +69,8 @@ namespace Robust.Client.UserInterface.Controls
public void SetMessage(FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
_message = message;
_entry = new RichTextEntry(_message, this, _tagManager, tagsAllowed, defaultColor);
_entry?.RemoveControls();
_entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor);
InvalidateMeasure();
}
@@ -73,31 +81,31 @@ namespace Robust.Client.UserInterface.Controls
SetMessage(msg, tagsAllowed, defaultColor);
}
public string? GetMessage() => _message?.ToMarkup();
public string? GetMessage() => _entry?.Message.ToMarkup();
/// <summary>
/// Returns a copy of the currently used formatted message.
/// </summary>
public FormattedMessage? GetFormattedMessage() => _entry == null ? null : new FormattedMessage(_entry.Value.Message);
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
if (_message == null)
{
if (_entry == null)
return Vector2.Zero;
}
var font = _getFont();
_entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
// _entry is nullable struct.
// cannot just call _entry.Value.Update() as that doesn't actually update _entry.
_entry = _entry.Value.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
return new Vector2(_entry.Value.Width / UIScale, _entry.Value.Height / UIScale);
}
protected internal override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (_message == null)
{
return;
}
_entry.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
_entry?.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
}
[Pure]
@@ -5,7 +5,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class BoldItalicTag : IMarkupTag
public sealed class BoldItalicTag : IMarkupTagHandler
{
public const string BoldItalicFont = "DefaultBoldItalic";
@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class BoldTag : IMarkupTag
public sealed class BoldTag : IMarkupTagHandler
{
public const string BoldFont = "DefaultBold";
@@ -2,7 +2,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class BulletTag : IMarkupTag
public sealed class BulletTag : IMarkupTagHandler
{
public string Name => "bullet";
@@ -6,7 +6,7 @@ namespace Robust.Client.UserInterface.RichText;
/// <summary>
/// Colors the text inside its opening and closing nodes
/// </summary>
public sealed class ColorTag : IMarkupTag
public sealed class ColorTag : IMarkupTagHandler
{
public static readonly Color DefaultColor = new(200, 200, 200);
@@ -8,14 +8,14 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class CommandLinkTag : IMarkupTag
public sealed class CommandLinkTag : IMarkupTagHandler
{
[Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!;
public string Name => "cmdlink";
/// <inheritdoc/>
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
if (!node.Value.TryGetString(out var text)
|| !node.Attributes.TryGetValue("command", out var commandParameter)
@@ -11,7 +11,7 @@ namespace Robust.Client.UserInterface.RichText;
/// Applies the font provided as the tags parameter to the markup drawing context.
/// Definitely not save for user supplied markup
/// </summary>
public sealed class FontTag : IMarkupTag
public sealed class FontTag : IMarkupTagHandler
{
public const string DefaultFont = "Default";
public const int DefaultSize = 12;
@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class HeadingTag : IMarkupTag
public sealed class HeadingTag : IMarkupTagHandler
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -1,9 +1,16 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public interface IMarkupTag
/// <summary>
/// Classes that implement this interface will be instantiated by <see cref="MarkupTagManager"/> and used to handle
/// the parsing and behaviour of markup tags. Note that each class is only ever instantiated once by the tag manager,
/// and wil be used to handle all tags of that kind, and thus should not contain state information relevant to a
/// specific tag.
/// </summary>
public interface IMarkupTagHandler
{
/// <summary>
/// The string used as the tags name when writing rich text
@@ -54,17 +61,32 @@ public interface IMarkupTag
}
/// <summary>
/// Called inside the constructor of <see cref="RichTextEntry"/> to
/// supply a control that gets rendered inline before this tags children<br/>
/// Text continues to the right of the control until the next line and then continues bellow it
/// Called inside the constructor of <see cref="RichTextEntry"/> to supply a control that gets rendered inline
/// before this tags children. The returned control must be new instance to avoid issues with shallow cloning
/// <see cref="FormattedMessage"/> nodes. Text continues to the right of the control until the next line and
/// then continues bellow it.
/// </summary>
/// <param name="node">The markup node containing the parameter and attributes</param>
/// <param name="control">A UI control for placing in line with this tags children</param>
/// <returns>true if this tag supplies a control</returns>
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
control = null;
return false;
}
}
[Obsolete("Use IMarkupTagHandler")]
public interface IMarkupTag : IMarkupTagHandler
{
bool IMarkupTagHandler.TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
return TryGetControl(node, out control);
}
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
control = null;
return false;
}
}
@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class ItalicTag : IMarkupTag
public sealed class ItalicTag : IMarkupTagHandler
{
public const string ItalicFont = "DefaultItalic";
@@ -16,7 +16,7 @@ public sealed class MarkupTagManager
/// <summary>
/// Tags defined in engine need to be instantiated here because of sandboxing
/// </summary>
private readonly Dictionary<string, IMarkupTag> _markupTagTypes = new IMarkupTag[] {
private readonly Dictionary<string, IMarkupTagHandler> _markupTagTypes = new IMarkupTagHandler[] {
new BoldItalicTag(),
new BoldTag(),
new BulletTag(),
@@ -44,13 +44,13 @@ public sealed class MarkupTagManager
public void Initialize()
{
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTag>())
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTagHandler>())
{
//Prevent tags defined inside engine from being instantiated
if (_engineTypes.Contains(type))
continue;
var instance = (IMarkupTag)_sandboxHelper.CreateInstance(type);
var instance = (IMarkupTagHandler)_sandboxHelper.CreateInstance(type);
_markupTagTypes[instance.Name.ToLower()] = instance;
}
@@ -60,22 +60,48 @@ public sealed class MarkupTagManager
}
}
[Obsolete("Use GetMarkupTagHandler")]
public IMarkupTag? GetMarkupTag(string name)
{
return _markupTagTypes.GetValueOrDefault(name) as IMarkupTag;
}
public IMarkupTagHandler? GetMarkupTagHandler(string name)
{
return _markupTagTypes.GetValueOrDefault(name);
}
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
/// <summary>
/// Attempt to get the tag handler with the corresponding name.
/// </summary>
/// <param name="name">The name of the tag, as specified by <see cref="IMarkupTag.Name"/></param>
/// <param name="tagsAllowed">List of allowed tag types. If null, all types are allowed.</param>
/// <param name="handler">The instance responsible for handling tags of this type.</param>
/// <returns></returns>
public bool TryGetMarkupTagHandler(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTagHandler? handler)
{
if (_markupTagTypes.TryGetValue(name, out var markupTag)
// Using a whitelist prevents new tags from sneaking in.
&& (tagsAllowed == null || Array.IndexOf(tagsAllowed, markupTag.GetType()) != -1))
{
tag = markupTag;
handler = markupTag;
return true;
}
tag = null;
handler = null;
return false;
}
[Obsolete("Use TryGetMarkupTagHandler")]
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
{
if (!TryGetMarkupTagHandler(name, tagsAllowed, out var handler) || handler is not IMarkupTag cast)
{
tag = null;
return false;
}
tag = cast;
return true;
}
}
+37 -11
View File
@@ -13,6 +13,9 @@ namespace Robust.Client.UserInterface
{
/// <summary>
/// Used by <see cref="OutputPanel"/> and <see cref="RichTextLabel"/> to handle rich text layout.
/// Note that if this text is ever removed or modified without removing the owning control,
/// then <see cref="RemoveControls"/> should be called to ensure that any controls that were added by this
/// entry are also removed.
/// </summary>
internal struct RichTextEntry
{
@@ -36,7 +39,7 @@ namespace Robust.Client.UserInterface
/// </summary>
public ValueList<int> LineBreaks;
private readonly Dictionary<int, Control>? _tagControls;
public readonly Dictionary<int, Control>? Controls;
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
@@ -56,15 +59,35 @@ namespace Robust.Client.UserInterface
if (node.Name == null)
continue;
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var handler) || !handler.TryCreateControl(node, out var control))
continue;
// Markup tag handler instances are shared across controls. We need to ensure that the hanlder doesn't
// store state information and return the same control for each rich text entry.
DebugTools.Assert(handler.TryCreateControl(node, out var other) && other != control);
parent.Children.Add(control);
tagControls ??= new Dictionary<int, Control>();
tagControls.Add(nodeIndex, control);
}
_tagControls = tagControls;
Controls = tagControls;
}
// TODO RICH TEXT
// Somehow ensure that this **has** to be called when removing rich text from some control.
/// <summary>
/// Remove all owned controls from their parents.
/// </summary>
public readonly void RemoveControls()
{
if (Controls == null)
return;
foreach (var ctrl in Controls.Values)
{
ctrl.Orphan();
}
}
/// <summary>
@@ -74,7 +97,7 @@ namespace Robust.Client.UserInterface
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
/// <param name="uiScale"></param>
/// <param name="lineHeightScale"></param>
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
public RichTextEntry Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
{
// This method is gonna suck due to complexity.
// Bear with me here.
@@ -112,10 +135,10 @@ namespace Robust.Client.UserInterface
continue;
if (ProcessMetric(ref this, metrics, out breakLine))
return;
return this;
}
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
continue;
control.Measure(new Vector2(Width, Height));
@@ -128,12 +151,14 @@ namespace Robust.Client.UserInterface
desiredSize.Y);
if (ProcessMetric(ref this, controlMetrics, out breakLine))
return;
return this;
}
Width = wordWrap.FinalizeText(out breakLine);
CheckLineBreak(ref this, breakLine);
return this;
bool ProcessRune(ref RichTextEntry src, Rune rune, out int? outBreakLine)
{
wordWrap.NextRune(rune, out breakLine, out var breakNewLine, out var skip);
@@ -166,9 +191,10 @@ namespace Robust.Client.UserInterface
internal readonly void HideControls()
{
if (_tagControls == null)
if (Controls == null)
return;
foreach (var control in _tagControls.Values)
foreach (var control in Controls.Values)
{
control.Visible = false;
}
@@ -220,7 +246,7 @@ namespace Robust.Client.UserInterface
globalBreakCounter += 1;
}
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
continue;
// Controls may have been previously hidden via HideControls due to being "out-of frame".
@@ -243,7 +269,7 @@ namespace Robust.Client.UserInterface
return node.Value.StringValue ?? "";
//Skip the node if there is no markup tag for it.
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var tag))
return "";
if (!node.Closing)