Link to reagent ingredients on the same Guidebook page (#36700)

* Add in-page links for guidebook reagent recipes

* Add links to microwave recipes

* This function is too specific to be in Control extensions

* Better naming

* Wrap RichTextLabel instead of subclassing

* "Activate" is ambiguous
This commit is contained in:
Ciarán Walsh
2025-05-09 01:06:26 +01:00
committed by GitHub
parent 7bec148634
commit 2a201837c7
9 changed files with 287 additions and 56 deletions

View File

@@ -19,13 +19,15 @@ namespace Content.Client.Guidebook.Controls;
/// Control for embedding a microwave recipe into a guidebook. /// Control for embedding a microwave recipe into a guidebook.
/// </summary> /// </summary>
[UsedImplicitly, GenerateTypedNameReferences] [UsedImplicitly, GenerateTypedNameReferences]
public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, ISearchableControl public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, ISearchableControl, IPrototypeRepresentationControl
{ {
[Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!; private ISawmill _sawmill = default!;
public IPrototype? RepresentedPrototype { get; private set; }
public GuideMicrowaveEmbed() public GuideMicrowaveEmbed()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
@@ -80,6 +82,8 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
{ {
var entity = _prototype.Index<EntityPrototype>(recipe.Result); var entity = _prototype.Index<EntityPrototype>(recipe.Result);
RepresentedPrototype = entity;
IconContainer.AddChild(new GuideEntityEmbed(recipe.Result, false, false)); IconContainer.AddChild(new GuideEntityEmbed(recipe.Result, false, false));
ResultName.SetMarkup(entity.Name); ResultName.SetMarkup(entity.Name);
ResultDescription.SetMarkup(entity.Description); ResultDescription.SetMarkup(entity.Description);
@@ -99,8 +103,9 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
solidNameMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-solid-name-display", ("ingredient", ingredient.Name))); solidNameMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-solid-name-display", ("ingredient", ingredient.Name)));
solidNameMsg.Pop(); solidNameMsg.Pop();
var solidNameLabel = new RichTextLabel(); var solidNameLabel = new GuidebookRichPrototypeLink();
solidNameLabel.SetMessage(solidNameMsg); solidNameLabel.SetMessage(solidNameMsg);
solidNameLabel.LinkedPrototype = ingredient;
IngredientsGrid.AddChild(solidNameLabel); IngredientsGrid.AddChild(solidNameLabel);
@@ -129,9 +134,10 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
liquidColorMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-reagent-color-display", ("color", reagent.SubstanceColor))); liquidColorMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-reagent-color-display", ("color", reagent.SubstanceColor)));
liquidColorMsg.Pop(); liquidColorMsg.Pop();
var liquidColorLabel = new RichTextLabel(); var liquidColorLabel = new GuidebookRichPrototypeLink();
liquidColorLabel.SetMessage(liquidColorMsg); liquidColorLabel.SetMessage(liquidColorMsg);
liquidColorLabel.HorizontalAlignment = Control.HAlignment.Center; liquidColorLabel.HorizontalAlignment = Control.HAlignment.Center;
liquidColorLabel.LinkedPrototype = reagent;
IngredientsGrid.AddChild(liquidColorLabel); IngredientsGrid.AddChild(liquidColorLabel);

View File

@@ -22,13 +22,15 @@ namespace Content.Client.Guidebook.Controls;
/// Control for embedding a reagent into a guidebook. /// Control for embedding a reagent into a guidebook.
/// </summary> /// </summary>
[UsedImplicitly, GenerateTypedNameReferences] [UsedImplicitly, GenerateTypedNameReferences]
public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISearchableControl public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISearchableControl, IPrototypeRepresentationControl
{ {
[Dependency] private readonly IEntitySystemManager _systemManager = default!; [Dependency] private readonly IEntitySystemManager _systemManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IPrototypeManager _prototype = default!;
private readonly ChemistryGuideDataSystem _chemistryGuideData; private readonly ChemistryGuideDataSystem _chemistryGuideData;
public IPrototype? RepresentedPrototype { get; private set; }
public GuideReagentEmbed() public GuideReagentEmbed()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
@@ -80,6 +82,8 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
private void GenerateControl(ReagentPrototype reagent) private void GenerateControl(ReagentPrototype reagent)
{ {
RepresentedPrototype = reagent;
NameBackground.PanelOverride = new StyleBoxFlat NameBackground.PanelOverride = new StyleBoxFlat
{ {
BackgroundColor = reagent.SubstanceColor BackgroundColor = reagent.SubstanceColor

View File

@@ -4,13 +4,11 @@
HorizontalExpand="True" HorizontalExpand="True"
Margin="0 0 0 5"> Margin="0 0 0 5">
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Horizontal">
<BoxContainer Name="ReactantsContainer" Orientation="Vertical" HorizontalExpand="True" <BoxContainer Orientation="Vertical" HorizontalExpand="True"
VerticalAlignment="Center"> VerticalAlignment="Center">
<RichTextLabel Name="ReactantsLabel" <BoxContainer Name="ReactantsContainer" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Vertical">
HorizontalAlignment="Center" <!-- Reactants will be added as children here -->
VerticalAlignment="Center" </BoxContainer>
Access="Public"
Visible="False" />
</BoxContainer> </BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center"> <BoxContainer Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextureRect TexturePath="/Textures/Interface/Misc/beakerlarge.png" <TextureRect TexturePath="/Textures/Interface/Misc/beakerlarge.png"
@@ -23,11 +21,9 @@
Margin="2 0 0 0" /> Margin="2 0 0 0" />
</BoxContainer> </BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalAlignment="Center"> <BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalAlignment="Center">
<RichTextLabel Name="ProductsLabel" <BoxContainer Name="ProductsContainer" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Vertical">
HorizontalAlignment="Center" <!-- Products will be added as children here -->
VerticalAlignment="Center" </BoxContainer>
Access="Public"
Visible="False" />
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" /> <PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />

View File

@@ -34,16 +34,16 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont
public GuideReagentReaction(ReactionPrototype prototype, IPrototypeManager protoMan, IEntitySystemManager sysMan) : this(protoMan) public GuideReagentReaction(ReactionPrototype prototype, IPrototypeManager protoMan, IEntitySystemManager sysMan) : this(protoMan)
{ {
var reactantsLabel = ReactantsLabel; Container container = ReactantsContainer;
SetReagents(prototype.Reactants, ref reactantsLabel, protoMan); SetReagents(prototype.Reactants, ref container, protoMan);
var productLabel = ProductsLabel; Container productContainer = ProductsContainer;
var products = new Dictionary<string, FixedPoint2>(prototype.Products); var products = new Dictionary<string, FixedPoint2>(prototype.Products);
foreach (var (reagent, reactantProto) in prototype.Reactants) foreach (var (reagent, reactantProto) in prototype.Reactants)
{ {
if (reactantProto.Catalyst) if (reactantProto.Catalyst)
products.Add(reagent, reactantProto.Amount); products.Add(reagent, reactantProto.Amount);
} }
SetReagents(products, ref productLabel, protoMan); SetReagents(products, ref productContainer, protoMan, false);
var mixingCategories = new List<MixingCategoryPrototype>(); var mixingCategories = new List<MixingCategoryPrototype>();
if (prototype.MixingCategories != null) if (prototype.MixingCategories != null)
@@ -85,8 +85,8 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont
entContainer.AddChild(nameLabel); entContainer.AddChild(nameLabel);
ReactantsContainer.AddChild(entContainer); ReactantsContainer.AddChild(entContainer);
var productLabel = ProductsLabel; Container productContainer = ProductsContainer;
SetReagents(solution.Contents, ref productLabel, protoMan); SetReagents(solution.Contents, ref productContainer, protoMan, false);
SetMixingCategory(categories, null, sysMan); SetMixingCategory(categories, null, sysMan);
} }
@@ -95,75 +95,80 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont
IPrototypeManager protoMan, IPrototypeManager protoMan,
IEntitySystemManager sysMan) : this(protoMan) IEntitySystemManager sysMan) : this(protoMan)
{ {
ReactantsLabel.Visible = true; var label = new RichTextLabel();
ReactantsLabel.SetMarkup(Loc.GetString("guidebook-reagent-sources-gas-wrapper", label.SetMarkup(Loc.GetString("guidebook-reagent-sources-gas-wrapper",
("name", Loc.GetString(prototype.Name).ToLower()))); ("name", Loc.GetString(prototype.Name).ToLower())));
ReactantsContainer.Visible = true;
ReactantsContainer.AddChild(label);
if (prototype.Reagent != null) if (prototype.Reagent != null)
{ {
var quantity = new Dictionary<string, FixedPoint2> var quantity = new Dictionary<string, FixedPoint2>
{ {
{ prototype.Reagent, FixedPoint2.New(0.21f) } { prototype.Reagent, FixedPoint2.New(0.21f) }
}; };
var productLabel = ProductsLabel; Container productContainer = ProductsContainer;
SetReagents(quantity, ref productLabel, protoMan); SetReagents(quantity, ref productContainer, protoMan, false);
} }
SetMixingCategory(categories, null, sysMan); SetMixingCategory(categories, null, sysMan);
} }
private void SetReagents(List<ReagentQuantity> reagents, ref RichTextLabel label, IPrototypeManager protoMan) private void SetReagents(List<ReagentQuantity> reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true)
{ {
var amounts = new Dictionary<string, FixedPoint2>(); var amounts = new Dictionary<string, FixedPoint2>();
foreach (var (reagent, quantity) in reagents) foreach (var (reagent, quantity) in reagents)
{ {
amounts.Add(reagent.Prototype, quantity); amounts.Add(reagent.Prototype, quantity);
} }
SetReagents(amounts, ref label, protoMan); SetReagents(amounts, ref container, protoMan, addLinks);
} }
private void SetReagents( private void SetReagents(
Dictionary<string, ReactantPrototype> reactants, Dictionary<string, ReactantPrototype> reactants,
ref RichTextLabel label, ref Container container,
IPrototypeManager protoMan) IPrototypeManager protoMan,
bool addLinks = true)
{ {
var amounts = new Dictionary<string, FixedPoint2>(); var amounts = new Dictionary<string, FixedPoint2>();
foreach (var (reagent, reactantPrototype) in reactants) foreach (var (reagent, reactantPrototype) in reactants)
{ {
amounts.Add(reagent, reactantPrototype.Amount); amounts.Add(reagent, reactantPrototype.Amount);
} }
SetReagents(amounts, ref label, protoMan); SetReagents(amounts, ref container, protoMan, addLinks);
} }
[PublicAPI] [PublicAPI]
private void SetReagents( private void SetReagents(
Dictionary<ProtoId<MixingCategoryPrototype>, ReactantPrototype> reactants, Dictionary<ProtoId<MixingCategoryPrototype>, ReactantPrototype> reactants,
ref RichTextLabel label, ref Container container,
IPrototypeManager protoMan) IPrototypeManager protoMan,
bool addLinks = true)
{ {
var amounts = new Dictionary<string, FixedPoint2>(); var amounts = new Dictionary<string, FixedPoint2>();
foreach (var (reagent, reactantPrototype) in reactants) foreach (var (reagent, reactantPrototype) in reactants)
{ {
amounts.Add(reagent, reactantPrototype.Amount); amounts.Add(reagent, reactantPrototype.Amount);
} }
SetReagents(amounts, ref label, protoMan); SetReagents(amounts, ref container, protoMan, addLinks);
} }
private void SetReagents(Dictionary<string, FixedPoint2> reagents, ref RichTextLabel label, IPrototypeManager protoMan) private void SetReagents(Dictionary<string, FixedPoint2> reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true)
{ {
var msg = new FormattedMessage();
var reagentCount = reagents.Count;
var i = 0;
foreach (var (product, amount) in reagents.OrderByDescending(p => p.Value)) foreach (var (product, amount) in reagents.OrderByDescending(p => p.Value))
{ {
var productProto = protoMan.Index<ReagentPrototype>(product);
var msg = new FormattedMessage();
msg.AddMarkupOrThrow(Loc.GetString("guidebook-reagent-recipes-reagent-display", msg.AddMarkupOrThrow(Loc.GetString("guidebook-reagent-recipes-reagent-display",
("reagent", protoMan.Index<ReagentPrototype>(product).LocalizedName), ("ratio", amount))); ("reagent", productProto.LocalizedName), ("ratio", amount)));
i++;
if (i < reagentCount) var label = new GuidebookRichPrototypeLink();
msg.PushNewline(); if (addLinks)
label.LinkedPrototype = productProto;
label.SetMessage(msg);
container.AddChild(label);
} }
msg.Pop(); container.Visible = true;
label.SetMessage(msg);
label.Visible = true;
} }
private void SetMixingCategory(IReadOnlyList<ProtoId<MixingCategoryPrototype>> mixingCategories, ReactionPrototype? prototype, IEntitySystemManager sysMan) private void SetMixingCategory(IReadOnlyList<ProtoId<MixingCategoryPrototype>> mixingCategories, ReactionPrototype? prototype, IEntitySystemManager sysMan)

View File

@@ -0,0 +1,71 @@
using Content.Client.Guidebook.RichText;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Content.Client.UserInterface.ControlExtensions;
namespace Content.Client.Guidebook.Controls;
/// <summary>
/// A RichTextLabel which is a link to a specified IPrototype.
/// The link is activated by the owner if the prototype is represented
/// somewhere in the same document.
/// </summary>
public sealed class GuidebookRichPrototypeLink : Control, IPrototypeLinkControl
{
private bool _linkActive = false;
private FormattedMessage? _message;
private readonly RichTextLabel _richTextLabel;
public void EnablePrototypeLink()
{
if (_message == null)
return;
_linkActive = true;
DefaultCursorShape = CursorShape.Hand;
_richTextLabel.SetMessage(_message, null, TextLinkTag.LinkColor);
}
public GuidebookRichPrototypeLink() : base()
{
MouseFilter = MouseFilterMode.Pass;
OnKeyBindDown += HandleClick;
_richTextLabel = new RichTextLabel();
AddChild(_richTextLabel);
}
public void SetMessage(FormattedMessage message)
{
_message = message;
_richTextLabel.SetMessage(_message);
}
public IPrototype? LinkedPrototype { get; set; }
private void HandleClick(GUIBoundKeyEventArgs args)
{
if (!_linkActive)
return;
if (args.Function != EngineKeyFunctions.UIClick)
return;
if (this.TryGetParentHandler<IAnchorClickHandler>(out var handler))
{
handler.HandleAnchor(this);
args.Handle();
}
else
Logger.Warning("Warning! No valid IAnchorClickHandler found.");
}
}
public interface IAnchorClickHandler
{
public void HandleAnchor(IPrototypeLinkControl prototypeLinkControl);
}

View File

@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Linq; using System.Linq;
using Content.Client.Guidebook.RichText; using Content.Client.Guidebook.RichText;
using Content.Client.UserInterface.ControlExtensions; using Content.Client.UserInterface.ControlExtensions;
@@ -6,6 +7,7 @@ using Content.Client.UserInterface.Controls.FancyTree;
using Content.Client.UserInterface.Systems.Info; using Content.Client.UserInterface.Systems.Info;
using Content.Shared.Guidebook; using Content.Shared.Guidebook;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.ContentPack; using Robust.Shared.ContentPack;
@@ -14,7 +16,7 @@ using Robust.Shared.Prototypes;
namespace Content.Client.Guidebook.Controls; namespace Content.Client.Guidebook.Controls;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IAnchorClickHandler
{ {
[Dependency] private readonly DocumentParsingManager _parsingMan = default!; [Dependency] private readonly DocumentParsingManager _parsingMan = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!; [Dependency] private readonly IResourceManager _resourceManager = default!;
@@ -53,6 +55,38 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
ShowGuide(entry); ShowGuide(entry);
} }
public void HandleAnchor(IPrototypeLinkControl prototypeLinkControl)
{
var prototype = prototypeLinkControl.LinkedPrototype;
if (prototype == null)
return;
var (linkableControls, _) = GetLinkableControlsAndLinks(EntryContainer);
foreach (var linkableControl in linkableControls)
{
if (linkableControl.RepresentedPrototype != prototype)
continue;
if (linkableControl is not Control control)
return;
// Check if the target item is currently filtered out
if (!control.Visible)
control.Visible = true;
UserInterfaceManager.DeferAction(() =>
{
if (control.GetControlScrollPosition() is not {} position)
return;
Scroll.HScrollTarget = position.X;
Scroll.VScrollTarget = position.Y;
});
break;
}
}
private void OnSelectionChanged(TreeItem? item) private void OnSelectionChanged(TreeItem? item)
{ {
if (item != null && item.Metadata is GuideEntry entry) if (item != null && item.Metadata is GuideEntry entry)
@@ -94,6 +128,23 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
} }
LastEntry = entry.Id; LastEntry = entry.Id;
var (linkableControls, linkControls) = GetLinkableControlsAndLinks(EntryContainer);
HashSet<IPrototype> availablePrototypeLinks = new();
foreach (var linkableControl in linkableControls)
{
var prototype = linkableControl.RepresentedPrototype;
if (prototype != null)
availablePrototypeLinks.Add(prototype);
}
foreach (var linkControl in linkControls)
{
var prototype = linkControl.LinkedPrototype;
if (prototype != null && availablePrototypeLinks.Contains(prototype))
linkControl.EnablePrototypeLink();
}
} }
public void UpdateGuides( public void UpdateGuides(
@@ -209,4 +260,30 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
} }
} }
} }
private static (List<IPrototypeRepresentationControl>, List<IPrototypeLinkControl>) GetLinkableControlsAndLinks(Control parent)
{
List<IPrototypeRepresentationControl> linkableList = new();
List<IPrototypeLinkControl> linkList = new();
foreach (var child in parent.Children)
{
var hasChildren = child.ChildCount > 0;
if (child is IPrototypeLinkControl linkChild)
linkList.Add(linkChild);
else if (child is IPrototypeRepresentationControl linkableChild)
linkableList.Add(linkableChild);
if (!hasChildren)
continue;
var (childLinkableList, childLinkList) = GetLinkableControlsAndLinks(child);
linkableList.AddRange(childLinkableList);
linkList.AddRange(childLinkList);
}
return (linkableList, linkList);
}
} }

View File

@@ -0,0 +1,28 @@
using Robust.Shared.Prototypes;
namespace Content.Client.Guidebook.Controls;
/// <summary>
/// Interface for controls which represent a Prototype
/// These can be linked to from a IPrototypeLinkControl
/// </summary>
public interface IPrototypeRepresentationControl
{
// The prototype that this control represents
public IPrototype? RepresentedPrototype { get; }
}
/// <summary>
/// Interface for controls which can be clicked to navigate
/// to a specified prototype representation on the same page.
/// </summary>
public interface IPrototypeLinkControl
{
// This control is a link to the specified prototype
public IPrototype? LinkedPrototype { get; }
// Initially the link will not be enabled,
// the owner can enable the link once there is a valid target
// for the Prototype link.
public void EnablePrototypeLink();
}

View File

@@ -5,12 +5,15 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.RichText; using Robust.Client.UserInterface.RichText;
using Robust.Shared.Input; using Robust.Shared.Input;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Content.Client.UserInterface.ControlExtensions;
namespace Content.Client.Guidebook.RichText; namespace Content.Client.Guidebook.RichText;
[UsedImplicitly] [UsedImplicitly]
public sealed class TextLinkTag : IMarkupTag public sealed class TextLinkTag : IMarkupTag
{ {
public static Color LinkColor => Color.CornflowerBlue;
public string Name => "textlink"; public string Name => "textlink";
public Control? Control; public Control? Control;
@@ -30,7 +33,7 @@ public sealed class TextLinkTag : IMarkupTag
label.Text = text; label.Text = text;
label.MouseFilter = Control.MouseFilterMode.Stop; label.MouseFilter = Control.MouseFilterMode.Stop;
label.FontColorOverride = Color.CornflowerBlue; label.FontColorOverride = LinkColor;
label.DefaultCursorShape = Control.CursorShape.Hand; label.DefaultCursorShape = Control.CursorShape.Hand;
label.OnMouseEntered += _ => label.FontColorOverride = Color.LightSkyBlue; label.OnMouseEntered += _ => label.FontColorOverride = Color.LightSkyBlue;
@@ -50,17 +53,10 @@ public sealed class TextLinkTag : IMarkupTag
if (Control == null) if (Control == null)
return; return;
var current = Control; if (Control.TryGetParentHandler<ILinkClickHandler>(out var handler))
while (current != null)
{
current = current.Parent;
if (current is not ILinkClickHandler handler)
continue;
handler.HandleClick(link); handler.HandleClick(link);
return; else
} Logger.Warning("Warning! No valid ILinkClickHandler found.");
Logger.Warning($"Warning! No valid ILinkClickHandler found.");
} }
} }

View File

@@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Content.Client.Guidebook.Controls; using Content.Client.Guidebook.Controls;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
@@ -68,6 +70,52 @@ public static class ControlExtension
return controlList; return controlList;
} }
/// <summary>
/// Search the controls tree for a parent node of type T
/// E.g. to find the control implementing some event handling interface.
/// </summary>
public static bool TryGetParentHandler<T>(this Control child, [NotNullWhen(true)] out T? result)
{
for (var control = child; control is not null; control = control.Parent)
{
if (control is not T handler)
continue;
result = handler;
return true;
}
result = default;
return false;
}
/// <summary>
/// Find the controls offset relative to its closest ScrollContainer
/// Returns null if the control is not in the tree or not visible.
/// </summary>
public static Vector2? GetControlScrollPosition(this Control child)
{
if (!child.VisibleInTree)
return null;
var position = new Vector2();
var control = child;
while (control is not null)
{
// The scroll container's direct child is re-positioned while scrolling,
// so we need to ignore its position.
if (control.Parent is ScrollContainer)
break;
position += control.Position;
control = control.Parent;
}
return position;
}
public static bool ChildrenContainText(this Control parent, string search) public static bool ChildrenContainText(this Control parent, string search)
{ {
var labels = parent.GetControlOfType<Label>(); var labels = parent.GetControlOfType<Label>();