UMG with super/subscript

I can’t for the life of me find any method by which to create super- or subscript text for the UMG. The Rich Text Block widget does most of the things I have needed so far, but I just want to be able to add super/subscript. The ability to shift the baseline in the RTB decorator would be perfect, but at this point I don’t really mind as long as it works!

Can anyone point me to something for UMG that will allow superscript and subscript?

1 Like

I’m afraid you have to write your own decorator. Come to think of it, I might have a need for just that for a sprint in a couple of weeks, so maybe I’ll take a shot at it. If I do so, I’ll post an implementation.

In the meantime, the URichTextBlockImageDecorator class is really the simplest example I can think of. I’d suggest starting from there, it’s what I would do.

So I got around to it and built a rich text decorator that formats super- and subscript. It’s pretty easy, really, complicated only by the fact that there’s hardly any documentation around.

You’ll find the complete code at the bottom of the post if you want to copy&paste and modify it for your own projects.

This is the text that was entered into the rich text block’s “Text” field:

This is H<sub>2</>O. It tastes nice<sup>[citation needed]</>.

And this is what it looks like on a widget:

It was accomplished using three assets:
assets
They are, from left to right:

  1. The decorator. This is a Blueprint asset that inherits from the custom class UMyRichTextBlockDecorator, which in turn inherits from the UE base class URichTextBlockDecorator. It gets added to the rich text block widget’s array of decorators in the UMG editor.
  2. The user widget. Just your regular old user widget. The rich text block is in there.
  3. The text style. Every rich text block needs a style asset in order to render. This is it. Note that it is not necessary to define styles for the super- and subscript tags in that asset.

To get the decorator working, we need to create three classes. First is the UMG decorator:

// Note: We could make the class not be abstract, in which case we wouldn't need the BP
// decorator at all. We would just plug the native class into the list of decorators.
UCLASS(Abstract, Blueprintable, meta = (DisplayName="My Rich Text Block Decorator"))
class UMyRichTextBlockDecorator : public URichTextBlockDecorator
{
	GENERATED_BODY()

public:
	// this is the only function we have to override
	virtual TSharedPtr<ITextDecorator> CreateDecorator(URichTextBlock* InOwner) override;
};

The second class inherits from FRichTextDecorator and implements the ITextDecorator interface from the function above.

class FMyRichTextDecorator : public FRichTextDecorator
{
public:
	FMyRichTextDecorator(URichTextBlock* InOwner);

	// we override this to define which tags are supported by this decorator
	virtual bool Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const override;

protected:
	// we override this to create the widget that displays 
	// the text that's marked up by our custom tags
	virtual TSharedPtr<SWidget> CreateDecoratorWidget(const FTextRunInfo& RunInfo, const FTextBlockStyle& TextStyle) const override;

private:
	// we use this to store all the tags that we support. Mostly for convenience.
	TArray<FString> SupportedTags;
};

The implementation of these two classes is dead simple:

FMyRichTextDecorator::FMyRichTextDecorator(URichTextBlock* InOwner)
	: FRichTextDecorator(InOwner)
{
	// the tags we want to support
	SupportedTags = { TEXT("sub"), TEXT("sup") };
}

bool FMyRichTextDecorator::Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const
{
	// we support any tag that we defined above
	return SupportedTags.Contains(RunParseResult.Name);
}

TSharedPtr<SWidget> FMyRichTextDecorator::CreateDecoratorWidget(const FTextRunInfo& RunInfo, const FTextBlockStyle& TextStyle) const
{
	// the decorator widget is the widget that gets "injected" into the
	// rich text block and that contains the formatted text. We will implement
	// this ourselves. It's where most of the actual work is done.
	return SNew(SRichInlineText, RunInfo.Content, RunInfo.Name, TextStyle);
}

TSharedPtr<ITextDecorator> UMyRichTextBlockDecorator::CreateDecorator(URichTextBlock* InOwner)
{
	// create the decorator struct that creates the widget
	return MakeShareable(new FMyRichTextDecorator(InOwner));
}

The third class is the one that does all of the work. Note that this is somewhat arbitrary. I decided to put all the logic in one place and for this place to be the Slate widget. If you look at the source code for the image decorator, you will notice that most of the work is done in the struct.


class SRichInlineText : public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(SRichInlineText)
	{}
	SLATE_END_ARGS()

	// pass the things we need to the Construct function. I decided on the text 
	// between the tags, the tag names, and the default text style
	void Construct(const FArguments& InArgs, const FText& Text, const FString& TagName, const FTextBlockStyle& TextStyle)
	{
		// don't do anything if the text between the tags is empty
		if (!Text.IsEmptyOrWhitespace())
		{
			// the line height is particular useful for laying out super- and
			// subscript text because we need to align a text block widget a little later
			const float LineHeight = FSlateApplication::Get().GetRenderer()->GetFontMeasureService()->GetMaxCharacterHeight(TextStyle.Font, 1.0f);

			// these couple of lines are the heart of all this. We check the tag and
			// set the appropriate layout parameters before we pass them to the widget
			FTextBlockStyle Style = TextStyle;
			EVerticalAlignment Align = VAlign_Fill;

			if (TagName == TEXT("sub"))
			{
				Style.Font.Size *= 0.5f;
				Align = VAlign_Bottom;
			}
			else if (TagName == TEXT("sup"))
			{
				Style.Font.Size *= 0.5f;
				Align = VAlign_Top;
			}

			// and this is where we pass the layout params to the rendered widget
			ChildSlot
			[
				SNew(SBox)
				.HeightOverride(LineHeight)
				.VAlign(Align)
				[
					SNew(STextBlock)
					.Text(Text)
					.TextStyle(&Style)
				]
			];
		}
	}
};

It is up to us to decide how to handle the different tags. We could create entirely different slot and widget layouts if a tag required it. Stuff like vertical boxes with two text boxes and a thin line between them for mathematical formulas for example.

Anyway, this is all that’s needed. There are a lot of nuances that I left out, like referencing a rich text style within the decorator and things like that. Look at this as an MVP of sorts. Here is the complete code:

.h

#pragma once

#include "CoreMinimal.h"
#include "Components/RichTextBlockDecorator.h"
#include "MyRichTextBlockDecorator.generated.h"

class FMyRichTextDecorator : public FRichTextDecorator
{
public:
	FMyRichTextDecorator(URichTextBlock* InOwner);
	virtual bool Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const override;

protected:
	virtual TSharedPtr<SWidget> CreateDecoratorWidget(const FTextRunInfo& RunInfo, const FTextBlockStyle& TextStyle) const override;

private:
	TArray<FString> SupportedTags;
};

UCLASS(Abstract, Blueprintable, meta = (DisplayName="My Rich Text Block Decorator"))
class UMyRichTextBlockDecorator : public URichTextBlockDecorator
{
	GENERATED_BODY()

public:
	virtual TSharedPtr<ITextDecorator> CreateDecorator(URichTextBlock* InOwner) override;
};

.cpp

#include "MyRichTextBlockDecorator.h"
#include "Fonts/FontMeasure.h"

class SRichInlineText : public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(SRichInlineText)
	{}
	SLATE_END_ARGS()

	void Construct(const FArguments& InArgs, const FText& Text, const FString& TagName, const FTextBlockStyle& TextStyle)
	{
		if (!Text.IsEmptyOrWhitespace())
		{
			const float LineHeight = FSlateApplication::Get().GetRenderer()->GetFontMeasureService()->GetMaxCharacterHeight(TextStyle.Font, 1.0f);

			FTextBlockStyle Style = TextStyle;
			EVerticalAlignment Align = VAlign_Fill;

			if (TagName == TEXT("sub"))
			{
				Style.Font.Size *= 0.5f;
				Align = VAlign_Bottom;
			}
			else if (TagName == TEXT("sup"))
			{
				Style.Font.Size *= 0.5f;
				Align = VAlign_Top;
			}

			ChildSlot
			[
				SNew(SBox)
				.HeightOverride(LineHeight)
				.VAlign(Align)
				[
					SNew(STextBlock)
					.Text(Text)
					.TextStyle(&Style)
				]
			];
		}
	}
};

FMyRichTextDecorator::FMyRichTextDecorator(URichTextBlock* InOwner)
	: FRichTextDecorator(InOwner)
{
	SupportedTags = { TEXT("sub"), TEXT("sup") };
}

bool FMyRichTextDecorator::Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const
{
	return SupportedTags.Contains(RunParseResult.Name);
}

TSharedPtr<SWidget> FMyRichTextDecorator::CreateDecoratorWidget(const FTextRunInfo& RunInfo, const FTextBlockStyle& TextStyle) const
{
	return SNew(SRichInlineText, RunInfo.Content, RunInfo.Name, TextStyle);
}

TSharedPtr<ITextDecorator> UMyRichTextBlockDecorator::CreateDecorator(URichTextBlock* InOwner)
{
	return MakeShareable(new FMyRichTextDecorator(InOwner));
}

5 Likes

That looks perfect! My C++ is close to non-existent, so thank you for commenting your way through it as well. I’ll throw it in as soon as I have a minute to work on that project.

Thank you so much for taking the time to get through this. From the Slate base, it seemed like a strange omission for something that’s been in HTML since it’s inception (though I work in science, so I may be biased).

Ok, I’ve found the solution:
go to \Source<projectname>/projectname.cs and uncomment PrivateDependencyModuleNames.AddRange(new string[] { “Slate”, “SlateCore” });

Hope it help the others :slight_smile:

1 Like

Hello,
I’m trying to implement your script but i’m a total beginner in C++
I’ve created a new c++ class inherited from richtextblockdecorator
I’ve read your article, copy pasted the final code into Myrichtextblockdecorator.h and .cpp,
But i’m getting those errors:

Did someone have a solution?