Tooltip not displayed when using FSlateHyperlinkRun::FOnGetTooltipText in SRichTextBlock

This posting is based on engine version 5.3

Symptom

  1. When we use SRichTextBlock
  2. If we designate the tooltip text by using FHyperlinkDecorator::Create() and FSlateHyperlinkRun::FOnGetTooltipText
  3. The tooltip is not displayed.

The detail is below:

I wanted to create an SRichTextBlock and have a simple tooltip appear when you hover the mouse over the hyperlink within it.

1
2
3
4
5
6
7
8
9
SNew(SRichTextBlock)
.DecoratorStyleSet(&FAppStyle::Get())
.Text(INVTEXT("SomeText5 <a id=\"browser\" href=\"https://hyaniner.com/\"> SomeLink </>"))
+ FHyperlinkDecorator::Create(
    TEXT("browser")
    , FSlateHyperlinkRun::FOnClick::CreateStatic(&OnBrowserLinkClicked)
    , FSlateHyperlinkRun::FOnGetTooltipText::CreateStatic(&SomeFunctionName)
    //InToolTipDelegate has default paramteter value, so we leave out.
);

I made the SomeFunctionName as like this:

1
2
3
4
static FText SomeFunctionName(const FSlateHyperlinkRun::FMetadata& Metadata)
{
    return SomeText;
}

But nothing came out. So I started debugging.

For reference, FHyperlinkDecorator::Create() looks like this.

(I wanted to make it easier to view on the homepage, so I artificially changed the line breaks a little.)

1
2
3
4
5
6
static SLATE_API TSharedRef< FHyperlinkDecorator > Create(
    FString Id
    , const FSlateHyperlinkRun::FOnClick& NavigateDelegate
    , const FSlateHyperlinkRun::FOnGetTooltipText& InToolTipTextDelegate = FSlateHyperlinkRun::FOnGetTooltipText()
    , const FSlateHyperlinkRun::FOnGenerateTooltip& InToolTipDelegate = FSlateHyperlinkRun::FOnGenerateTooltip() 
);

Cause

Note

For reference, from below:

  • Some content is omitted to reduce the length of exposed engine code. Please go to the GitHub link to see the exact code.
  • To help you understand, there are parts where I added comments.

Circumstances of SWidget

void SWidget::SWidgetConstruct(const FSlateBaseNamedArgs& Args) has the code below.

GitHub Link
1
2
3
4
5
6
7
8
if (Args._ToolTip.IsSet())
{
    SetToolTip(Args._ToolTip);
}
else if (Args._ToolTipText.IsSet())
{
    SetToolTipText(Args._ToolTipText);
}

If ToolTip is given, ToolTipText will be ignored.

Circumstances of FSlateHyperlinkRun

The FHyperlinkDecorator object created by FHyperlinkDecorator::Create() is passed to SRichTextBlock.

SRichTextBlock calls FSlateHyperlinkRun::CreateBlock() based on this in the first prepass and generates the result in the format of TSharedRef< ILayoutBlock >.

The call stack at this time is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
FSlateHyperlinkRun::CreateBlock(int, int, TVector2<…>, const FLayoutBlockTextContext &, const TSharedPtr<…> &) SlateHyperlinkRun.cpp:66
FTextLayout::FRunModel::CreateBlock(const FTextLayout::FBlockDefinition &, float, const FLayoutBlockTextContext &) TextLayout.cpp:2715
FTextLayout::CreateLineViewBlocks(int, const int, const float, const TOptional<…> &, int &, int &, int &, TArray<…> &) TextLayout.cpp:330
FTextLayout::FlowLineLayout(const int, const float, TArray<…> &) TextLayout.cpp:588
FTextLayout::FlowLayout() TextLayout.cpp:543
FTextLayout::UpdateLayout() TextLayout.cpp:1208
FTextLayout::UpdateIfNeeded() TextLayout.cpp:1191
FSlateTextBlockLayout::ComputeDesiredSize(const FSlateTextBlockLayout::FWidgetDesiredSizeArgs &, const float) SlateTextBlockLayout.cpp:124
[inline] FSlateTextBlockLayout::ComputeDesiredSize(const FSlateTextBlockLayout::FWidgetDesiredSizeArgs &, const float, const FTextBlockStyle &) SlateTextBlockLayout.cpp:132
SRichTextBlock::ComputeDesiredSize(float) SRichTextBlock.cpp:90

At this time, the code of FSlateHyperlinkRun::CreateBlock() is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
FText ToolTipText;//If we set the breakpoint here, it makes debugging easier.
TSharedPtr<IToolTip> ToolTip;//Constructed as nullptr

if(TooltipDelegate.IsBound())//This is false when FSlateHyperlinkRun::FOnGenerateTooltip() is passed as the default value.
{
    ToolTip = TooltipDelegate.Execute(RunInfo.MetaData);
}
else
{
...
/* The part where ToolTipText is determined. For more details, see GitHub. */
...
}

TSharedRef< SWidget > Widget = SNew( SRichTextHyperlink, ViewModel )
    .Style( &Style )
    .Text( FText::FromString( FString( EndIndex - StartIndex, **Text + StartIndex ) ) )
    .ToolTip( ToolTip )//It is set as nullptr, and it is passed as the type of TAttribute<>.
    .ToolTipText( ToolTipText )
    .OnNavigate( this, &FSlateHyperlinkRun::OnNavigate )
    .TextShapingMethod( TextContext.TextShapingMethod );

The problem is that TSharedPtr<IToolTip> ToolTip is not TAttribute<>.

The details are below:

  1. The local variable ToolTip is constructed, as the type of TSharedPtr<IToolTip>. It is nullptr.
  2. In the part of constructing SRichTextHyperlink, for .ToolTip( ToolTip ), TSharedPtr<IToolTip> ToolTip that is nullptr, is passed.
  3. Based on this value set, TAttribute<TSharedPtr<IToolTip>> is created and passed to SWidget as Args._ToolTip.
  4. InSWidget, Args._ToolTip.IsSet() is true. Even if its value is nullptr, anyway, the value has been set.
  5. So the code of SetToolTip(Args._ToolTip); is run, and SetToolTipText(Args._ToolTipText);is skipped.
  6. But the value is nullptr. so there is nothing to display.

Workaround

Without modifying engine, we can not use const FSlateHyperlinkRun::FOnGetTooltipText& InToolTipTextDelegate.

So I decided to use const FSlateHyperlinkRun::FOnGenerateTooltip& InToolTipDelegate.

I changed the code about creating a delegate in the construction SRichTextBlock , like below.

1
2
3
4
5
6
7
8
9
SNew(SRichTextBlock)
.DecoratorStyleSet(&FAppStyle::Get())
.Text(INVTEXT("SomeText5 <a id=\"browser\" href=\"https://hyaniner.com/\"> SomeLink </>"))
+ FHyperlinkDecorator::Create(
    TEXT("browser")
    , FSlateHyperlinkRun::FOnClick::CreateStatic(&OnBrowserLinkClicked)
    , FSlateHyperlinkRun::FOnGetTooltipText()
    , FSlateHyperlinkRun::FOnGenerateTooltip::CreateStatic(&OnGenerateTooltip)
);

And the function that the delegate will call is like below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
TSharedRef<IToolTip> SomeClassName::OnGenerateTooltip(const FSlateHyperlinkRun::FMetadata& Metadata)
{
    FText Result = /*Content of tooltip*/;

    return SNew( SToolTip )
    .Text(Result)
    .IsInteractive(false)
    .BorderImage( FCoreStyle::Get().GetBrush("ToolTip.BrightBackground") )
    .TextMargin(FMargin(11.0f))
    ;
}

It was displayed well.

Things I want to be careful of in the future

My thought was like this: When creating and using a Slate widget class based on SCompoundWidget, it occurred to me that if there are many different cases that need to be considered and confusion is likely to occur, and if there is a need to pass parameters with TAttribute<>, will it be safer to keep the type of variables as TAttribute<> for the relevant items? I thought I’d make a similar mistake someday.

tags