A TextBlock has an option AutoWrapText and the option makes the TextBlock can wrap its text. Thanks to the option, we can display a text without concerning about breaking lines. For general cases of text, even the option works within very short time, almost 1 tick. How does it possible ? What is the implementation of that option ? Let us find out it in this post.
The TextBlock upper has the option turned on. Contrary, the TextBlock lower has the option turned off.
Where is the code
1 2 3 4 5
// TextWidgetTypes.h
/** True if we're wrapping text automatically based on the computed horizontal space for this widget. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Wrapping) uint8 AutoWrapText:1;
The option is loacted in the class UTextLayoutWidget. We can see the option as the class UTextBlock inherites UTextLayoutWidget. Unfortunately, the variable is not directly used for wrapping text, but used for saving the value of option.
When you turn on or turn off the option AutoWrapText, widget’s SynchronizeProperties() would be called. By the code Super::SynchronizeTextLayoutProperties(*MyTextBlock); executed, Parent’s SynchronizeProperties(TWidgetType&) is called.
1 2 3 4 5 6 7 8 9 10 11 12 13
/** Synchronize the properties with the given widget. A template as the Slate widgets conform to the same API, but don't derive from a common base. */ template <typename TWidgetType> voidSynchronizeTextLayoutProperties(TWidgetType& InWidget) { ShapedTextOptions.SynchronizeShapedTextProperties(InWidget);
Good. The parameter InAutoWrapText updates the variable AutoWrapText and MyTextBlock. The variable MyTextBlock is TSharedPtr<STextBlock>. Now, the time to jump to STextBlock.
Here, in STextBlock the variable AutoWrapText holds the value of option. The function Assign() just saves the value its inside. The value of AutoWrapText is used in two positions.
// Account for the outline width impacting both size of the text by multiplying by 2 // Outline size in Y is accounted for in MaxHeight calculation in Measure() const FVector2D ComputedOutlineSize(LocalOutlineSize * 2.f, LocalOutlineSize); const FVector2D TextSize = FSlateApplication::Get().GetRenderer()->GetFontMeasureService()->Measure(BoundText.Get(), GetFont()) + ComputedOutlineSize + LocalShadowOffset;
CachedSimpleDesiredSize = FVector2f(FMath::Max(MinDesiredWidth.Get(), TextSize.X), TextSize.Y); returnFVector2D(CachedSimpleDesiredSize.GetValue()); } else { // ComputeDesiredSize will also update the text layout cache if required const FVector2D TextSize = TextLayoutCache->ComputeDesiredSize( FSlateTextBlockLayout::FWidgetDesiredSizeArgs( BoundText.Get(), HighlightText.Get(), WrapTextAt.Get(), AutoWrapText.Get(), WrappingPolicy.Get(), GetTransformPolicyImpl(), Margin.Get(), LineHeightPercentage.Get(), Justification.Get()), LayoutScaleMultiplier, GetComputedTextStyle());
// Callstack UnrealEditor-Slate.dll!STextBlock::ComputeDesiredSize(float LayoutScaleMultiplier) Line 300 C++ UnrealEditor-SlateCore.dll!SWidget::CacheDesiredSize(float InLayoutScaleMultiplier) Line 936 C++ UnrealEditor-SlateCore.dll!SWidget::Prepass_Internal(float InLayoutScaleMultiplier) Line 1714 C++ [Inline Frame] UnrealEditor-SlateCore.dll!SWidget::Prepass_ChildLoop::__l2::<lambda_a0677895c4614612fd5b4c5f4771eae9>::operator()(SWidget &) Line 1751 C++ UnrealEditor-SlateCore.dll!FChildren::ForEachWidget<<lambda_a0677895c4614612fd5b4c5f4771eae9>>(SWidget::Prepass_ChildLoop::__l2::<lambda_a0677895c4614612fd5b4c5f4771eae9> Pred) Line 67 C++ [Inline Frame] UnrealEditor-SlateCore.dll!SWidget::Prepass_ChildLoop(float) Line 1721 C++ UnrealEditor-SlateCore.dll!SWidget::Prepass_Internal(float InLayoutScaleMultiplier) Line 1708 C++ ... UnrealEditor-SlateCore.dll!SWidget::Prepass_Internal(float InLayoutScaleMultiplier) Line 1708 C++ [Inline Frame] UnrealEditor-SlateCore.dll!SWidget::Prepass_ChildLoop::__l2::<lambda_a0677895c4614612fd5b4c5f4771eae9>::operator()(SWidget &) Line 1751 C++ UnrealEditor-SlateCore.dll!FChildren::ForEachWidget<<lambda_a0677895c4614612fd5b4c5f4771eae9>>(SWidget::Prepass_ChildLoop::__l2::<lambda_a0677895c4614612fd5b4c5f4771eae9> Pred) Line 67 C++ [Inline Frame] UnrealEditor-SlateCore.dll!SWidget::Prepass_ChildLoop(float) Line 1721 C++ UnrealEditor-SlateCore.dll!SWidget::Prepass_Internal(float InLayoutScaleMultiplier) Line 1708 C++ UnrealEditor-SlateCore.dll!SWidget::SlatePrepass(float InLayoutScaleMultiplier) Line 690 C++ UnrealEditor-Slate.dll!PrepassWindowAndChildren(TSharedRef<SWindow,1> WindowToPrepass) Line 1197 C++ UnrealEditor-Slate.dll!FSlateApplication::DrawPrepass(TSharedPtr<SWindow,1> DrawOnlyThisWindow) Line 1249 C++ UnrealEditor-Slate.dll!FSlateApplication::PrivateDrawWindows(TSharedPtr<SWindow,1> DrawOnlyThisWindow) Line 1294 C++ UnrealEditor-Slate.dll!FSlateApplication::DrawWindows() Line 1060 C++ UnrealEditor-Slate.dll!FSlateApplication::TickAndDrawWidgets(float DeltaTime) Line 1625 C++ UnrealEditor-Slate.dll!FSlateApplication::Tick(ESlateTickType TickType) Line 1482 C++ UnrealEditor.exe!FEngineLoop::Tick() Line 5325 C++ [Inline Frame] UnrealEditor.exe!EngineTick() Line 62 C++ UnrealEditor.exe!GuardedMain(constwchar_t * CmdLine) Line 183 C++ UnrealEditor.exe!GuardedMainWrapper(constwchar_t * CmdLine) Line 147 C++ UnrealEditor.exe!LaunchWindowsStartup(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow, constwchar_t * CmdLine) Line 283 C++ UnrealEditor.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * pCmdLine, int nCmdShow) Line 330 C++ [External Code]
// OnPaint will also update the text layout cache if required LayerId = TextLayoutCache->OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, ShouldBeEnabled(bParentEnabled));
// HACK: Due to the nature of wrapping and layout, we may have been arranged in a different box than what we were cached with. Which // might update wrapping, so make sure we always set the desired size to the current size of the text layout, which may have changed // during paint. constbool bCanWrap = WrapTextAt.Get() > 0 || AutoWrapText.Get();
if (bCanWrap && !NewDesiredSize.Equals(LastDesiredSize)) { const_cast<STextBlock*>(this)->Invalidate(EInvalidateWidgetReason::Layout); } }
return LayerId; }
// Callstack UnrealEditor-Slate.dll!STextBlock::OnPaint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 255 C++ UnrealEditor-SlateCore.dll!SWidget::Paint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 1543 C++ UnrealEditor-SlateCore.dll!SPanel::PaintArrangedChildren(const FPaintArgs & Args, const FArrangedChildren & ArrangedChildren, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 31 C++ UnrealEditor-SlateCore.dll!SPanel::OnPaint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 12 C++ UnrealEditor-SlateCore.dll!SWidget::Paint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 1543 C++ ... UnrealEditor-SlateCore.dll!SPanel::PaintArrangedChildren(const FPaintArgs & Args, const FArrangedChildren & ArrangedChildren, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 31 C++ UnrealEditor-SlateCore.dll!SPanel::OnPaint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 12 C++ UnrealEditor-SlateCore.dll!SWidget::Paint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 1543 C++ UnrealEditor-SlateCore.dll!SOverlay::OnPaint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 200 C++ UnrealEditor-SlateCore.dll!SWidget::Paint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 1543 C++ UnrealEditor-SlateCore.dll!SCompoundWidget::OnPaint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 46 C++ UnrealEditor-SlateCore.dll!SWidget::Paint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 1543 C++ UnrealEditor-SlateCore.dll!SWindow::PaintSlowPath(const FSlateInvalidationContext & Context) Line 2073 C++ UnrealEditor-SlateCore.dll!FSlateInvalidationRoot::PaintInvalidationRoot(const FSlateInvalidationContext & Context) Line 399 C++ UnrealEditor-SlateCore.dll!SWindow::PaintWindow(double CurrentTime, float DeltaTime, FSlateWindowElementList & OutDrawElements, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 2105 C++ UnrealEditor-Slate.dll!FSlateApplication::DrawWindowAndChildren(const TSharedRef<SWindow,1> & WindowToDraw, FDrawWindowArgs & DrawWindowArgs) Line 1106 C++ UnrealEditor-Slate.dll!FSlateApplication::PrivateDrawWindows(TSharedPtr<SWindow,1> DrawOnlyThisWindow) Line 1338 C++ UnrealEditor-Slate.dll!FSlateApplication::DrawWindows() Line 1060 C++ UnrealEditor-Slate.dll!FSlateApplication::TickAndDrawWidgets(float DeltaTime) Line 1625 C++ UnrealEditor-Slate.dll!FSlateApplication::Tick(ESlateTickType TickType) Line 1482 C++ UnrealEditor.exe!FEngineLoop::Tick() Line 5325 C++ [Inline Frame] UnrealEditor.exe!EngineTick() Line 62 C++ UnrealEditor.exe!GuardedMain(constwchar_t * CmdLine) Line 183 C++ UnrealEditor.exe!GuardedMainWrapper(constwchar_t * CmdLine) Line 147 C++ UnrealEditor.exe!LaunchWindowsStartup(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow, constwchar_t * CmdLine) Line 283 C++ UnrealEditor.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * pCmdLine, int nCmdShow) Line 330 C++ [External Code]
Second, an execution flow by Paint.
The flows are branched at FSlateApplication::PrivateDrawWindows(). In the function, DrawPrepass() is called at line #1292, and DrawWindowAndChildren() is called at line #1338. Respectively, Prepass and Paint. Engine just invalidate the widget in Paint flow, so we only need to look into Prepass flow.
FVector2D FSlateTextBlockLayout::ComputeDesiredSize(const FWidgetDesiredSizeArgs& InWidgetArgs, constfloat InScale, const FTextBlockStyle& InTextStyle) { // Cache the wrapping rules so that we can recompute the wrap at width in paint. CachedWrapTextAt = InWidgetArgs.WrapTextAt; bCachedAutoWrapText = InWidgetArgs.AutoWrapText;
// Set the text layout information TextLayout->SetScale(InScale); TextLayout->SetWrappingWidth(CalculateWrappingWidth()); TextLayout->SetWrappingPolicy(InWidgetArgs.WrappingPolicy); TextLayout->SetTransformPolicy(InWidgetArgs.TransformPolicy); TextLayout->SetMargin(InWidgetArgs.Margin); TextLayout->SetJustification(InWidgetArgs.Justification); TextLayout->SetLineHeightPercentage(InWidgetArgs.LineHeightPercentage);
// Has the transform policy changed? If so we need a full refresh as that is destructive to the model text if (PreviousTransformPolicy != TextLayout->GetTransformPolicy()) { Marshaller->MakeDirty(); }
// Has the style used for this text block changed? if (!IsStyleUpToDate(InTextStyle)) { TextLayout->SetDefaultTextStyle(InTextStyle); Marshaller->MakeDirty(); // will regenerate the text using the new default style }
{ bool bRequiresTextUpdate = false; const FText& TextToSet = InWidgetArgs.Text; if (!TextLastUpdate.IdenticalTo(TextToSet)) { // The pointer used by the bound text has changed, however the text may still be the same - check that now if (!TextLastUpdate.IsDisplayStringEqualTo(TextToSet)) { // The source text has changed, so update the internal text bRequiresTextUpdate = true; }
// Update this even if the text is lexically identical, as it will update the pointer compared by IdenticalTo for the next Tick TextLastUpdate = FTextSnapshot(TextToSet); }
if (bRequiresTextUpdate || Marshaller->IsDirty()) { UpdateTextLayout(TextToSet); } }
{ const FText& HighlightTextToSet = InWidgetArgs.HighlightText; if (!HighlightTextLastUpdate.IdenticalTo(HighlightTextToSet)) { // The pointer used by the bound text has changed, however the text may still be the same - check that now if (!HighlightTextLastUpdate.IsDisplayStringEqualTo(HighlightTextToSet)) { UpdateTextHighlights(HighlightTextToSet); }
// Update this even if the text is lexically identical, as it will update the pointer compared by IdenticalTo for the next Tick HighlightTextLastUpdate = FTextSnapshot(HighlightTextToSet); } }
// We need to update our size if the text layout has become dirty TextLayout->UpdateIfNeeded();
return TextLayout->GetSize(); }
The function FSlateTextBlockLayout::ComputeDesiredSize() is called during Prepass flow. Here, bCachedAutoWrapText caches the value of InWidgetArgs.AutoWrapText. This will be used at CalculateWrappingWidth() later.
1 2 3 4 5 6 7 8 9 10 11 12
floatFSlateTextBlockLayout::CalculateWrappingWidth()const { // Text wrapping can either be used defined (WrapTextAt), automatic (bAutoWrapText and CachedSize), // or a mixture of both. Take whichever has the smallest value (>1) float WrappingWidth = CachedWrapTextAt; if (bCachedAutoWrapText && CachedSize.X >= 1.0f) { WrappingWidth = (WrappingWidth >= 1.0f) ? FMath::Min(WrappingWidth, CachedSize.X) : CachedSize.X; }
return FMath::Max(0.0f, WrappingWidth); }
The CachedWrapTextAt will be the same with the value set by option WrapTextAt in editor. And, the CachedSize depends on the size of panel where the TextBlock resides in. In the example we are using, the variables would have a value like below:
CachedWrapTextAt = 0
CachedSize.X = 100
Because the width of SizeBox is 100 and we set the option WrapTextAt as 0. The function determines the length of wrapping, but it is not for the logic about how to divide texts or how to break lines. So, look back on FSlateTextBlockLayout::ComputeDesiredSize().
if ( bHasChangedLayout ) { // if something has changed then create a new View UpdateLayout(); }
// If the layout has changed, we always need to update the highlights if ( bHasChangedLayout || bHasChangedHighlights) { UpdateHighlights(); } }
In the function, there is some code to call FTextLayout::UpdateIfNeeded(). Oh, the UpdateLayout() looks like the one we wanted. The code will be executed when bHasChangedLayout is true, and the value is usually set by SetWrappingWidth().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
voidFTextLayout::SetWrappingWidth( float Value ) { constbool WasWrapping = WrappingWidth > 0.0f; constbool IsWrapping = Value > 0.0f;
if ( WrappingWidth != Value ) { WrappingWidth = Value; DirtyFlags |= ETextLayoutDirtyState::Layout;
if ( WasWrapping != IsWrapping ) { // Changing from wrapping/not-wrapping will affect the wrapping information for *all lines* // Clear out the entire cache so it gets regenerated on the text call to FlowLayout DirtyAllLineModels(ELineModelDirtyState::WrappingInformation); } } }
Suppose you switch the option AutoWrapText from false into true. Here, DirtyFlags will flag the ETextLayoutDirtyState::Layout, which is 1. Therefore, !!(DirtyFlags & ETextLayoutDirtyState::Layout) turns into 1. The bHasChangedLayout becomes 1, too.
// If we've not yet been provided with a custom line break iterator, then just use the default one if (!LineBreakIterator.IsValid()) { LineBreakIterator = FBreakIterator::CreateLineBreakIterator(); }
The LinBreakIterator is a line break iterator using the implementation of ICU(International Components for Unicode)’s break iterator. The break iterator does a job of finding a location of boundaries in text. Visit here for more details. To summarize, the break iterator can find where each word ends. For example, we have a text of Text Block Test and the break iterator can find locations just like this Text (HERE)Block (HERE)Test(HERE). So, let us see how it works.
// For Hangul using per-word wrapping, we walk forward to the last Hangul character in the word and use that as the starting point for the // line-break iterator, as this will correctly handle the remaining Geumchik wrapping rules, without also causing per-syllable wrapping if (GetHangulTextWrappingMethod() == EHangulTextWrappingMethod::PerWord) { CharIt.setIndex32(InternalPosition);
if (IsHangul(CharIt.current32())) { // Walk to the end of the Hangul characters while (CharIt.hasNext() && IsHangul(CharIt.next32())) { InternalPosition = CharIt.getIndex(); } } }
/** * Advance the iterator to the first boundary following the specified offset. * The value returned is always greater than the offset or * the value BreakIterator.DONE * @param offset the offset to begin scanning. * @return The first boundary after the specified offset. * @stable ICU 2.0 */ virtualint32_tfollowing(int32_t offset)= 0;
The InternalPosition is passed into following and it is the code of ICU library.
structFBreakCandidate { /** Range inclusive of trailing whitespace, as used to visually display and interact with the text */ FTextRange ActualRange; /** Range exclusive of trailing whitespace, as used to perform wrapping on a word boundary */ FTextRange TrimmedRange; /** Measured size inclusive of trailing whitespace, as used to visually display and interact with the text */ FVector2D ActualSize; /** Measured width exclusive of trailing whitespace, as used to perform wrapping on a word boundary */ float TrimmedWidth; /** If this break candidate has trailing whitespace, this is the width of the first character of the trailing whitespace */ float FirstTrailingWhitespaceCharWidth;
A FBreakCandidate will be inserted into BreakCandidates each iteration. It seems the FBreakCandidate knows the size of word (or a part of text). What happened in CreateBreakCandidate() ? How could they know the actual size of text ?
FTextLayout::FBreakCandidate FTextLayout::CreateBreakCandidate( int32& OutRunIndex, FLineModel& Line, int32 PreviousBreak, int32 CurrentBreak ) { ... // We need to consider the Runs when detecting and measuring the text lengths of Lines because // the font style used makes a difference. const int32 FirstRunIndexChecked = OutRunIndex; for (; OutRunIndex < Line.Runs.Num(); OutRunIndex++) { FRunModel& Run = Line.Runs[ OutRunIndex ]; const FTextRange Range = Run.GetTextRange();
if ( BeginIndex == StopIndex ) { // This slice is empty, no need to adjust anything SliceSize = SliceSizeWithoutTrailingWhitespace = FVector2D::ZeroVector; } elseif ( BeginIndex == WhitespaceStopIndex ) { // This slice contains only whitespace, no need to adjust SliceSizeWithoutTrailingWhitespace SliceSize = Run.Measure( BeginIndex, StopIndex, Scale, RunTextContext ); SliceSizeWithoutTrailingWhitespace = FVector2D::ZeroVector; } elseif ( WhitespaceStopIndex != StopIndex ) { // This slice contains trailing whitespace, measure the text size, then add on the whitespace size SliceSize = SliceSizeWithoutTrailingWhitespace = Run.Measure( BeginIndex, WhitespaceStopIndex, Scale, RunTextContext ); constfloat WhitespaceWidth = Run.Measure( WhitespaceStopIndex, StopIndex, Scale, RunTextContext ).X; SliceSize.X += WhitespaceWidth;
// We also need to measure the width of the first piece of trailing whitespace if ( WhitespaceStopIndex + 1 == StopIndex ) { // Only have one piece of whitespace FirstTrailingWhitespaceCharWidth = WhitespaceWidth; } else { // Deliberately use the run version of Measure as we don't want the run model to cache this measurement since it may be out of order and break the binary search FirstTrailingWhitespaceCharWidth = Run.GetRun()->Measure( WhitespaceStopIndex, WhitespaceStopIndex + 1, Scale, RunTextContext ).X; } } else { // This slice contains no whitespace, both sizes are the same and can use the same measurement SliceSize = SliceSizeWithoutTrailingWhitespace = Run.Measure( BeginIndex, StopIndex, Scale, RunTextContext ); } ... }
The CreateBreakCandidate() function is quite big size, about 200 lines. But the core of function is to calculate a size of slice. Do you remember the variable CurrentBreak that indicates where each slice ends ? Here, the function make a slice according to CurrentBreak and trim it. Trimming happens in while statement, which decreases the WhitespaceStopIndex until it indicates an end of last word.
The WhitespaceStopIndex would be 4 in our test text. That is because the index of first whitespace is 4 in Text Block Test. Eventually, we will enter the function Measure() as the slice is not empty. The only case that Measure() not called is when BeginIndex == StopIndex is true, in other words CurrentBreak == 0.
// Offset the measured shaped text by the outline since the outline was not factored into the size of the text // Need to add the outline offsetting to the beginning and the end because it surrounds both sides. constfloat ScaledOutlineSize = Style.Font.OutlineSettings.OutlineSize * Scale; const FVector2D OutlineSizeToApply((BeginIndex == Range.BeginIndex ? ScaledOutlineSize : 0) + (EndIndex == Range.EndIndex ? ScaledOutlineSize : 0), ScaledOutlineSize);
// Use the full text range (rather than the run range) so that text that spans runs will still be shaped correctly return ShapedTextCacheUtil::MeasureShapedText(TextContext.ShapedTextCache, FCachedShapedTextKey(FTextRange(0, Text->Len()), Scale, TextContext, Style.Font), FTextRange(BeginIndex, EndIndex), **Text) + ShadowOffsetToApply + OutlineSizeToApply; }
We will get a FVector2D from FSlateTextRun::Measure(), which is the size of slice. The code Run->Measure() is the same with calling ShapedTextCacheUtil::MeasureShapedText() when you are using a TextBlock. Calculating shadow offset is not important in this post, so we need to focus on ShapedTextCacheUtil::MeasureShapedText().
// ShapedTextCache.cpp FVector2D ShapedTextCacheUtil::MeasureShapedText(const FShapedTextCacheRef& InShapedTextCache, const FCachedShapedTextKey& InRunKey, const FTextRange& InMeasureRange, const TCHAR* InText) { // Get the shaped text for the entire run and try and take a sub-measurement from it - this can help minimize the amount of text shaping that needs to be done when measuring text FShapedGlyphSequenceRef ShapedText = InShapedTextCache->FindOrAddShapedText(InRunKey, InText);
// Couldn't measure the sub-range, try and measure from a shape of the specified range ShapedText = InShapedTextCache->FindOrAddShapedText(MeasureKey, InText); MeasuredWidth = ShapedText->GetMeasuredWidth(); }
As you can see, the FShapedGlyphSequenceRef is a shared reference of FShapedGlyphSequence. Then, what the hell is FShapedGlyphSequence ? And what it does ?
// FontCache.h /** Information for rendering a shaped text sequence */ classSLATECORE_API FShapedGlyphSequence { ... /** Array of glyphs in this sequence. This data will be ordered so that you can iterate and draw left-to-right, which means it will be backwards for right-to-left languages */ TArray<FShapedGlyphEntry> GlyphsToRender; ...
/** Information for rendering one glyph in a shaped text sequence */ structFShapedGlyphEntry { ... /** The index of this glyph from the source text. The source indices may skip characters if the sequence contains ligatures, additionally, some characters produce multiple glyphs leading to duplicate source indices */ int32 SourceIndex = 0; /** The amount to advance in X before drawing the next glyph in the sequence */ int16 XAdvance = 0; ...
// Track unique font faces if (CurrentGlyph.FontFaceData->FontFace.IsValid()) { GlyphFontFaces.AddUnique(CurrentGlyph.FontFaceData->FontFace); }
// Update the measured width SequenceWidth += CurrentGlyph.XAdvance; ...
The FShapedGlyphSequence has a TArray of FShapedGlyphEntry. And the FShapedGlyphEntry has several properties such as SourceIndex and XAdvance. Looks like the FShapedGlyphEntry has properties responding each character in text, and the FShapedGlyphSequence has properties responding whole text. The properties are for how to render the text appropriately. So here, we can regard the term Glyph as one single character.
// If the given font can't render that character (as the fallback font may be missing), try again with the fallback character if (CurrentChar != 0 && GlyphIndex == 0) { GlyphIndex = FT_Get_Char_Index(KerningOnlyTextSequenceEntry.FaceAndMemory->GetFace(), SlateFontRendererUtils::InvalidSubChar); }
Usually, the XAdvande is determined at FSlateTextShaper::PerformKerningOnlyTextShaping(). Engine uses the FreeType library for getting a estimated size of character when it rendered. The GlyphIndex is calculated based on font and character value.
// No cached data, go ahead and add an entry for it... const FT_Error Error = FT_Get_Advance(Face, InGlyphIndex, LoadFlags, &OutCachedAdvance); if (Error == 0) { if (!FT_IS_SCALABLE(Face) && FT_HAS_FIXED_SIZES(Face)) { // Fixed size fonts don't support scaling, but we calculated the scale to use for the glyph in ApplySizeAndScale OutCachedAdvance = FT_MulFix(OutCachedAdvance, ((LoadFlags & FT_LOAD_VERTICAL_LAYOUT) ? Face->size->metrics.y_scale : Face->size->metrics.x_scale)); }
The code AdvanceCache->FindOrCache(GlyphIndex, CachedAdvanceData) finds at cache, but it creates new one and cache it if could not find. The FT_Get_Advance() returns the result with parameter &OutCachedAdvance. We can get a size of single character through the function because the value GlyphIndex includes information of font and character value.
In our test text Text Block Test, the result is like below:
Index
Character
GlyphIndex
XAdvance
0
T
55
18
1
e
72
17
2
x
91
16
3
t
87
11
4
3
8
5
B
37
20
6
l
79
9
7
o
82
18
8
c
70
17
9
k
78
17
10
3
8
11
T
55
18
12
e
72
17
13
s
86
17
14
t
87
11
You can see that the same character has the same XAdvance value. For example, The character T has 55 of GlyphIndex and 18 of XAdvance. Go back to the ShapedTextCacheUtil::MeasureShapedText(), that is why the MeasuredWidth has a value of 220 ≒ 222 = 18 + 17 + ... + 17 + 11. The difference 2 occurs by the kerning.
The final width may differ a little bit because some combination of characters need a kerning. For example, though e and k have the same XAdvance value 17, a combination Te has a small size than a combination Tk. Because in the combination Te, e can stick to T closer than k in Tk. In other words, a character T can have XAdvance of 17 in the combinations such as Ta/Tc/Td, and so on. Otherwise such as Tb/Tf/Th, it can have XAdvance of 18.
Break lines (3/3); Creating lines with wrapping
Go back to the FTextLayout::CreateLineWrappingCache(), now we can wrap text according to size (exactly, width) of each slice. All slices are stored at the container BreakCandidates. In our test text Text Block Test, the result is like below:
BreakCandidates
ActualRange
TrimmedRange
0
Text [0, 5)
Text [0, 4)
1
Block [5, 11)
Block [5, 10)
2
Test [11, 15)
Test [11, 15)
※ [0, 5) is equal to [0, 4]
Do you remember there is a code calls FTextLayout::FlowLineLayout() in FTextLayout::FlowLayout() ?
Here, we accumulate a width of each BreakCandidate on CurrentWidth. And wrapping text occurs whenever CurrentWidth almost reaches to WrappingDrawWidth.
const FBreakCandidate& FinalBreakOnSoftLine = ( !IsFirstBreak && !IsFirstBreakOnSoftLine && !BreakWithoutTrailingWhitespaceDoesFit ) ? LineModel.BreakCandidates[ --BreakIndex ] : Break; // We want the wrapped line width to contain the first piece of trailing whitespace for a line, however we only do this if we have trailing whitespace // otherwise very long non-breaking words can cause the wrapped line width to expand beyond the desired wrap width float WrappedLineWidth = CurrentWidth; if ( BreakWithoutTrailingWhitespaceDoesFit ) { // This break has trailing whitespace WrappedLineWidth += ( FinalBreakOnSoftLine.TrimmedWidth + FinalBreakOnSoftLine.FirstTrailingWhitespaceCharWidth ); } else { // This break is longer than the wrapping point, so make sure and clamp the line size to the given wrapping width WrappedLineWidth += FinalBreakOnSoftLine.ActualSize.X; WrappedLineWidth = FMath::Min(WrappedLineWidth, WrappingDrawWidth); }
// We want wrapped lines to ignore any trailing whitespace when justifying // If FinalBreakOnSoftLine isn't the current Break, then the size of FinalBreakOnSoftLine (including its trailing whitespace) will have already // been added to CurrentWidth, so we need to remove that again before adding the trimmed width (which is the width we should justify with) // We should not attempt to adjust the last break on a soft-line as that might have explicit trailing whitespace TOptional<float> JustifiedLineWidth; if ( &FinalBreakOnSoftLine != &LineModel.BreakCandidates.Last() ) { JustifiedLineWidth = CurrentWidth - (&FinalBreakOnSoftLine == &Break ? 0.0f : FinalBreakOnSoftLine.ActualSize.X) + FinalBreakOnSoftLine.TrimmedWidth; }
Usually, when wrapping text needed, the codes above would be executed. FinalBreakOnSoftLine indicates the BreakCandidate that needs a new line after itself. In our test text Text Block Test, Text could be assigned.
// Update the soft line bounds based on this new block (needed within this loop due to bi-directional text, as the extents of the line array are not always the start and end of the range) const FTextRange& BlockRange = OutSoftLine.Last()->GetTextRange(); SoftLineRange.BeginIndex = FMath::Min(SoftLineRange.BeginIndex, BlockRange.BeginIndex); SoftLineRange.EndIndex = FMath::Max(SoftLineRange.EndIndex, BlockRange.EndIndex); } ... FTextLayout::FLineView LineView; LineView.Offset = CurrentOffset; LineView.Size = LineSize; LineView.TextHeight = UnscaleLineHeight; LineView.JustificationWidth = JustificationWidth.Get(LineView.Size.X); LineView.Range = SoftLineRange; LineView.TextBaseDirection = LineModel.TextBaseDirection; LineView.ModelIndex = LineModelIndex; LineView.Blocks.Append( OutSoftLine );
LineViews.Add( LineView ); ...
The function FTextLayout::CreateLineViewBlocks() creates new FTextLayout::FLineView and adds it into initialized LineViews. We already cleared the LineViews at the function FTextLayout::ClearView(). In our test txt, after all process, the LineViews will have the value like below:
LineViews
Range
0
[0, 5)
1
[5, 11)
2
[11, 15)
Finally, we found that the result of wrapping text. All of prerequisites are for splitting a text. Now we understand how the text can be wrapped in UnrealEngine.
Wrap-up
Text wrapping in UnrealEngine can be divided into 3 major steps.
Separating a text into slices Find where each word ends using ICU library. Separate text into slices based on the indices.
Measuring size of each slice Estimate size of rendered character using FreeType library. Apply several modifications such as kerning, shadow, and so on.
Creating lines with wrapping Add width until it reaches the wrapping width. When it reaches, create new line.