Fix OnAttachedChanged Lag when having a long chat history (#46050)

* I fucking hate chat code and UI code

* while I'm at it I'll make this prettier

* docs

* typo

* testfail

* reset chat upon joining game

* move code

* remove unused imports and add doc

* doc

* final commit

* oop

* Revert "reset chat upon joining game"

This reverts commit a851319521193d099b6d1571631b63917500bc47.

* review stuff

* testfail
This commit is contained in:
cashew 2026-09-19 21:10:03 +00:00 • committed by GitHub
parent 32b60ae36c
commit 86adeec6db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 132 additions and 57 deletions

View file

@ -1,13 +1,85 @@
using Content.Client.UserInterface.ControlExtensions;
using Content.Client.UserInterface.RichText;
using Content.Shared.Chat;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// Carries link data parsed and resolved from a TextLink <see cref="MarkupNode"/>,
/// only one field per link should be populated at a time.
/// only one LinkType field per link should be populated at a time.
/// </summary>
public sealed class TextLinkLabel : Label
public sealed partial class TextLinkLabel : Label
{
public string? LinkString { get; init; }
public NetEntity? LinkEntity { get; init; }
public string? LinkString { get; init; } // default links
public NetEntity? LinkEntity { get; init; } // entity links
public Color LinkColor { get; set; }
[Dependency] private IEntityManager _entity = default!;
private bool canClickLink;
public TextLinkLabel()
{
IoCManager.InjectDependencies(this);
OnKeyBindDown += OnKeybindDown;
OnMouseEntered += _ => OnHoverChanged(true);
OnMouseExited += _ => OnHoverChanged(false);
}
/// <summary>
/// Refreshes the label's properties based on the current link target
/// and the viewer's permission to follow it. <br />Call after construction,
/// and whenever that permission could have changed.
/// </summary>
/// <param name="visible">Whether the label should be shown at all. Defaults to true.</param>
/// <param name="clickable">Additional override to force the label non-clickable. Defaults to true.</param>
public void UpdateLabelProperties(SharedChatSystem chatSystem, bool? visible = null, bool? clickable = null)
{
visible ??= true;
Visible = visible.Value;
clickable ??= true;
canClickLink = (LinkString != null || (LinkEntity is { } netEntity && chatSystem.CanClickMessageSender(netEntity))) && (bool)clickable;
MouseFilter = canClickLink ? MouseFilterMode.Stop : MouseFilterMode.Ignore;
DefaultCursorShape = canClickLink ? CursorShape.Hand : CursorShape.Arrow;
OnHoverChanged(false);
}
private void OnHoverChanged(bool hovering)
{
FontColorOverride = (canClickLink, hovering) switch
{
(true, true) => Color.LightSkyBlue, // clickable and currently hovered
_ => LinkColor, // not clickable, or not hovered
};
}
/// <summary>
/// Delegates click to the nearest ancestor ILinkClickHandler or IEntityLinkClickHandler;
/// TextLinkLabel has no idea what a click actually does.
/// </summary>
private void OnKeybindDown(GUIBoundKeyEventArgs args)
{
if (args.Function != EngineKeyFunctions.UIClick)
return;
if (LinkString is null && LinkEntity is null)
return;
if (LinkEntity is { } entity &&
this.TryGetParentHandler<IEntityLinkClickHandler>(out var entityLinkClickHandler))
{
entityLinkClickHandler.HandleClick(entity);
}
else if (LinkString != null && this.TryGetParentHandler<ILinkClickHandler>(out var linkClickHandler))
{
linkClickHandler.HandleClick(LinkString);
}
}
}

View file

@ -24,12 +24,9 @@ public sealed partial class TextLinkTag
if (!NetEntity.TryParse(entStr, out var netEntity))
return false;
var chat = _entity.System<SharedChatSystem>();
var clickable = chat.CanClickMessageSender(netEntity);
var color = GetLinkColor(node, netEntity);
data = new LinkData(LinkString: null, LinkEntity: netEntity, Color: color, Clickable: clickable);
data = new LinkData(LinkString: null, LinkEntity: netEntity, Color: color);
return true;
}

View file

@ -1,4 +1,4 @@
using Robust.Shared.Utility;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface.RichText;
@ -15,7 +15,7 @@ public sealed partial class TextLinkTag
return false;
}
data = new LinkData(LinkString: linkStr, LinkEntity: null, Color: null, Clickable: true);
data = new LinkData(LinkString: linkStr, LinkEntity: null, Color: null);
return true;
}
}

View file

@ -4,20 +4,25 @@ using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Input;
using Robust.Shared.Utility;
using Content.Client.UserInterface.ControlExtensions;
using Content.Client.UserInterface.Controls;
using Content.Shared.Chat;
using Robust.Client.ResourceManagement;
namespace Content.Client.UserInterface.RichText;
/// <summary>
/// Markup tag handler for <c>[textlink="LinkText"]</c> nodes. Renders a link
/// <see cref="Label"/> in rich text, covering two types:
/// plain links (<c>link=</c>) and entity links (<c>entity=</c>).
/// optional <c>color=</c> and <c>entitynamecolor=</c> parameters
/// allow setting a color override and opting into using entity name colors for entity links
/// <see cref="Label"/> in rich text, covering two link types:
/// <list type="bullet">
/// <item><description>link="GuideEntryPrototypeID" — a plain link.</description></item>
/// <item><description>entity="NetEntity" — an entity link.</description></item>
/// </list>
/// Optional parameters:
/// <list type="bullet">
/// <item><description>color="HexColor" — color override.</description></item>
/// <item><description>entitynamecolor="Bool" — entity links only; opt into using the entity's name color.</description></item>
/// </list>
/// </summary>
[UsedImplicitly]
public sealed partial class TextLinkTag : IMarkupTagHandler
@ -25,6 +30,7 @@ public sealed partial class TextLinkTag : IMarkupTagHandler
[Dependency] private IEntityManager _entity = default!;
[Dependency] private IUserInterfaceManager _ui = default!;
[Dependency] private IResourceCache _cache = default!;
private SharedChatSystem? _chat;
public string Name => "textlink";
public static Color DefaultLinkColor => Color.CornflowerBlue;
@ -38,7 +44,7 @@ public sealed partial class TextLinkTag : IMarkupTagHandler
/// <summary>
/// Resolved Link Data, LinkString and LinkEntity should not be populated at the same time
/// </summary>
private readonly record struct LinkData(string? LinkString, NetEntity? LinkEntity, Color? Color, bool Clickable);
private readonly record struct LinkData(string? LinkString, NetEntity? LinkEntity, Color? Color);
public TextLinkTag()
{
@ -67,25 +73,27 @@ public sealed partial class TextLinkTag : IMarkupTagHandler
foreach (var (attrname, resolver) in _resolvers)
{
if (node.Attributes.ContainsKey(attrname))
if (!node.Attributes.ContainsKey(attrname))
continue;
if(!resolver(node, out linkData))
{
if(!resolver(node, out linkData))
{
return false;
}
linkTypeResolved = true;
break;
return false;
}
linkTypeResolved = true;
break;
}
if (!linkTypeResolved)
{
return false;
}
// color= > resolver-supplied color > default
var linkColor = ResolveColorOverride(node) ?? linkData.Color ?? DefaultLinkColor;
var linkLabel = new TextLinkLabel() { Text = text, LinkString = linkData.LinkString, LinkEntity = linkData.LinkEntity };
linkLabel.FontColorOverride = linkColor;
var linkLabel = new TextLinkLabel() { Text = text, LinkString = linkData.LinkString, LinkEntity = linkData.LinkEntity, LinkColor = linkColor};
// eat my ass about where this magic number comes from
// our UI stack is awful. Finding this magic number was awful.
@ -96,14 +104,8 @@ public sealed partial class TextLinkTag : IMarkupTagHandler
linkLabel.FontOverride = boldFont;
}
if (linkData.Clickable)
{
linkLabel.MouseFilter = Control.MouseFilterMode.Stop;
linkLabel.DefaultCursorShape = Control.CursorShape.Hand;
linkLabel.OnMouseEntered += _ => linkLabel.FontColorOverride = Color.LightSkyBlue;
linkLabel.OnMouseExited += _ => linkLabel.FontColorOverride = linkColor;
linkLabel.OnKeyBindDown += args => OnKeybindDown(args, linkLabel);
}
_chat ??= _entity.System<SharedChatSystem>();
linkLabel.UpdateLabelProperties(_chat);
control = linkLabel;
return true;
@ -119,28 +121,6 @@ public sealed partial class TextLinkTag : IMarkupTagHandler
return Color.TryFromHex(colorStr, out var color) ? color : null;
}
/// <summary>
/// Delegates to the nearest ancestor ILinkClickHandler or IEntityLinkClickHandler;
/// TextLinkTag has no idea what a click actually does.
/// </summary>
private void OnKeybindDown(GUIBoundKeyEventArgs args, TextLinkLabel? control)
{
if (args.Function != EngineKeyFunctions.UIClick)
return;
if (control == null)
return;
if (control.LinkEntity is { } entity && control.TryGetParentHandler<IEntityLinkClickHandler>(out var entityLinkClickHandler))
{
entityLinkClickHandler.HandleClick(entity);
}
else if (control.LinkString != null && control.TryGetParentHandler<ILinkClickHandler>(out var linkClickHandler))
{
linkClickHandler.HandleClick(control.LinkString);
}
}
}
/// <summary>

View file

@ -67,6 +67,7 @@ public sealed partial class ChatUIController : UIController
[UISystemDependency] private readonly MindSystem? _mindSystem = default!;
[UISystemDependency] private readonly RoleCodewordSystem? _roleCodewordSystem = default!;
private SharedChatSystem? _sharedChatSys;
private static readonly ProtoId<ColorPalettePrototype> ChatNamePalette = "ChatNames";
private string[] _chatNameColors = default!;
private bool _chatNameColorsEnabled;
@ -429,7 +430,7 @@ public sealed partial class ChatUIController : UIController
{
UpdateChannelPermissions();
UpdateAutoFillHighlights();
Repopulate();
UpdateLinkLabels();
}
private void AddSpeechBubble(ChatMessage msg, SpeechBubble.SpeechType speechType)
@ -942,6 +943,16 @@ public sealed partial class ChatUIController : UIController
}
}
private void UpdateLinkLabels()
{
_sharedChatSys ??= _ent.System<SharedChatSystem>();
foreach (var chat in _chats)
{
chat.UpdateTextLinkLabelProperties(_sharedChatSys);
}
}
/// <summary>
/// Returns the chat name color for a mob
/// </summary>

View file

@ -1,3 +1,4 @@
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.RichText;
using Content.Client.UserInterface.Systems.Chat.Controls;
using Content.Shared.Chat;
@ -91,6 +92,20 @@ public partial class ChatBox : UIWidget, IEntityLinkClickHandler
}
}
/// <summary>
/// Calls update function on every child <see cref="TextLinkLabel"/> in the ChatBox's OutputPanel "Contents"
/// </summary>
public void UpdateTextLinkLabelProperties(SharedChatSystem _sharedChatSys)
{
foreach (var contentsChild in Contents.Children)
{
if (contentsChild is TextLinkLabel textLinkLabel)
{
textLinkLabel.UpdateLabelProperties(_sharedChatSys);
}
}
}
private void OnChannelFilter(ChatChannel channel, bool active)
{
Contents.Clear();