You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
==========================
MAJOR FEATURES + CHANGES
==========================
Change 2817214 on 2016/01/06 by mason.seay
Adjusted Walkable Slope Override for mesh
#jira UE-24473
Change 2817384 on 2016/01/06 by Michael.Schoell
Crash fix when selecting a variable node for a variable that is not owned by a Blueprint.
#jira UE-24958 - Crash when getting the sequence player in level blueprint
Change 2817438 on 2016/01/06 by Max.Chen
Sequencer: Add option to specify position of material name from the movie scene capture interface. For example, MovieCapture_{material}_{width}x{height}.{frame} will create files like this: MovieCapture_FinalImage_1920x1080.0010.exr
#rb Andrew.Rodham
#jira UE-24926
Change 2817459 on 2016/01/06 by Marc.Audy
PR #1679: Move MinRespawnDelay to virtual method AController::GetMinRespawnDelay() (Contributed by bozaro)
#jira UE-22309
Change 2817472 on 2016/01/06 by Ben.Marsh
Always run UHT in unattended mode from UBT; we don't want it opening any dialogs. Match3 is currently missing a plugin, and it's causing builds to time out.
Change 2817473 on 2016/01/06 by Marc.Audy
PR #1644: Improve "SpawnActor failed because the spawned actor IsPendingKill" error message (Contributed by slonopotamus)
#jira UE-21911
Change 2817533 on 2016/01/06 by Lauren.Ridge
Fixing Match3 not compiling in Debug (removed two checks on TileLibrary)
#jira UE-25004
Change 2817625 on 2016/01/06 by Taizyd.Korambayil
#jira UE-19659 Reimported Template Animations with Proper Skeletons
Change 2817647 on 2016/01/06 by Lukasz.Furman
replaced ensure during initialization of blackboard based behavior tree task with log warning
#ue4
#jira UE-24448
#rb Mieszko.Zielinski
Change 2817648 on 2016/01/06 by Lukasz.Furman
fixed broken rendering component of navmesh actor after delete-undo operation
#ue4
#jira UE-24446
#rb Mieszko.Zielinski
Change 2817688 on 2016/01/06 by Taizyd.Korambayil
#jira UE-22347 Fixed Message Warnings on Startup
Change 2817815 on 2016/01/06 by Jamie.Dale
Multiple fixes when editing right-to-left text
- Text is now shaped over the entire line to allow rich-text and selected text to be shaped correctly across block boundaries.
- Text layout highlights are now able to correctly handle bi-directional and right-to-left text.
- Text picking can now handle bi-directional and right-to-left text.
- Text picking can now pick the individual characters that make up a ligature glyph.
- The caret now draws on the logical (rather than visual) side of the glyph (to handle right-to-left text).
- Glyph clusters (multiple glyphs produced from a single character) are now treated as a single logical glyph.
- Optimized some of the FShapedGlyphSequence to allow an early out once they've found and processed the start and end glyphs.
#jira UE-25013
Change 2817828 on 2016/01/06 by Nick.Darnell
Editor - Fixing the OpenLauncher call to be take a structure to allow us to customize it more, and to properly handle the silent command the way we're planning to handle it in the launcher.
#jira UE-24563
Change 2818052 on 2016/01/06 by Nick.Darnell
Editor - Adding another application check for the launcher to catch the current app name on mac.
#jira UE-24563
Change 2818149 on 2016/01/06 by Taizyd.Korambayil
#jira UE-19097 Adjusted FirstPerson Pawn, so that Camera doesnt clip the Arm Mesh
Change 2818360 on 2016/01/06 by Chris.Babcock
Fix reading from ini sections not cached after build system changes for 4.11
#jira UE-25027
#ue4
#android
Change 2818369 on 2016/01/06 by Ryan.Vance
#jira UE-24976
Adding tessellation support to instanced stereo
Change 2818999 on 2016/01/07 by Robert.Manuszewski
UHT will no longer try to load game-only plugins.
#jira UE-25032
- Changed module type RuntimeNoProgram to RuntimeAndProgram so that bu default Runtime plugin modules won't be loaded by programs
- Added better error message when UHT's PreInit fails
Change 2819064 on 2016/01/07 by Richard.Hinckley
#jira UE-24694
Fixing array usage in 4.11 stream.
Change 2819067 on 2016/01/07 by Ori.Cohen
When editor tries to spawn a physics asset we automatically load the needed skeletal mesh
#rb Matt.K
#JIRA UE-24165
2470 lines
78 KiB
C++
2470 lines
78 KiB
C++
// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SlatePrivatePCH.h"
|
|
#include "BreakIterator.h"
|
|
#include "ShapedTextCache.h"
|
|
|
|
|
|
static TAutoConsoleVariable<int32> CVarDefaultTextFlowDirection(
|
|
TEXT("Slate.DefaultTextFlowDirection"),
|
|
static_cast<int32>(ETextFlowDirection::Auto),
|
|
TEXT("0: Auto (default), 1: LeftToRight, 2: RightToLeft."),
|
|
ECVF_Default
|
|
);
|
|
|
|
ETextFlowDirection GetDefaultTextFlowDirection()
|
|
{
|
|
const int32 DefaultTextFlowDirectionAsInt = CVarDefaultTextFlowDirection.AsVariable()->GetInt();
|
|
if (DefaultTextFlowDirectionAsInt >= static_cast<int32>(ETextFlowDirection::Auto) && DefaultTextFlowDirectionAsInt <= static_cast<int32>(ETextFlowDirection::RightToLeft))
|
|
{
|
|
return static_cast<ETextFlowDirection>(DefaultTextFlowDirectionAsInt);
|
|
}
|
|
return ETextFlowDirection::Auto;
|
|
}
|
|
|
|
|
|
FTextLayout::FBreakCandidate FTextLayout::CreateBreakCandidate( int32& OutRunIndex, FLineModel& Line, int32 PreviousBreak, int32 CurrentBreak )
|
|
{
|
|
const FRunTextContext RunTextContext(TextShapingMethod, Line.TextBaseDirection, Line.ShapedTextCache);
|
|
|
|
bool SuccessfullyMeasuredSlice = false;
|
|
int16 MaxAboveBaseline = 0;
|
|
int16 MaxBelowBaseline = 0;
|
|
FVector2D BreakSize( ForceInitToZero );
|
|
FVector2D BreakSizeWithoutTrailingWhitespace( ForceInitToZero );
|
|
float FirstTrailingWhitespaceCharWidth = 0.0f;
|
|
int32 WhitespaceStopIndex = CurrentBreak;
|
|
uint8 Kerning = 0;
|
|
|
|
if ( Line.Runs.IsValidIndex( OutRunIndex ) )
|
|
{
|
|
FRunModel& Run = Line.Runs[ OutRunIndex ];
|
|
const FTextRange Range = Run.GetTextRange();
|
|
int32 BeginIndex = FMath::Max( PreviousBreak, Range.BeginIndex );
|
|
|
|
if ( BeginIndex > 0 )
|
|
{
|
|
Kerning = Run.GetKerning( BeginIndex, Scale, RunTextContext );
|
|
}
|
|
}
|
|
|
|
// We need to consider the Runs when detecting and measuring the text lengths of Lines because
|
|
// the font style used makes a difference.
|
|
for (; OutRunIndex < Line.Runs.Num(); OutRunIndex++)
|
|
{
|
|
FRunModel& Run = Line.Runs[ OutRunIndex ];
|
|
const FTextRange Range = Run.GetTextRange();
|
|
|
|
FVector2D SliceSize;
|
|
FVector2D SliceSizeWithoutTrailingWhitespace;
|
|
int32 StopIndex = PreviousBreak;
|
|
|
|
WhitespaceStopIndex = StopIndex = FMath::Min( Range.EndIndex, CurrentBreak );
|
|
int32 BeginIndex = FMath::Max( PreviousBreak, Range.BeginIndex );
|
|
|
|
while( WhitespaceStopIndex > BeginIndex && FText::IsWhitespace( (*Line.Text)[ WhitespaceStopIndex - 1 ] ) )
|
|
{
|
|
--WhitespaceStopIndex;
|
|
}
|
|
|
|
if ( BeginIndex == StopIndex )
|
|
{
|
|
// This slice is empty, no need to adjust anything
|
|
SliceSize = SliceSizeWithoutTrailingWhitespace = FVector2D::ZeroVector;
|
|
}
|
|
else if ( BeginIndex == WhitespaceStopIndex )
|
|
{
|
|
// This slice contains only whitespace, no need to adjust SliceSizeWithoutTrailingWhitespace
|
|
SliceSize = Run.Measure( BeginIndex, StopIndex, Scale, RunTextContext );
|
|
SliceSizeWithoutTrailingWhitespace = FVector2D::ZeroVector;
|
|
}
|
|
else if ( 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 );
|
|
const float 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 );
|
|
}
|
|
|
|
BreakSize.X += SliceSize.X; // We accumulate the slice widths
|
|
BreakSizeWithoutTrailingWhitespace.X += SliceSizeWithoutTrailingWhitespace.X; // We accumulate the slice widths
|
|
|
|
// Get the baseline and flip it's sign; Baselines are generally negative
|
|
const int16 Baseline = -(Run.GetBaseLine( Scale ));
|
|
|
|
// For the height of the slice we need to take into account the largest value below and above the baseline and add those together
|
|
MaxAboveBaseline = FMath::Max( MaxAboveBaseline, (int16)( Run.GetMaxHeight( Scale ) - Baseline ) );
|
|
MaxBelowBaseline = FMath::Max( MaxBelowBaseline, Baseline );
|
|
|
|
if ( StopIndex == CurrentBreak )
|
|
{
|
|
SuccessfullyMeasuredSlice = true;
|
|
|
|
if ( OutRunIndex < Line.Runs.Num() && StopIndex == Line.Runs[ OutRunIndex ].GetTextRange().EndIndex )
|
|
{
|
|
++OutRunIndex;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
check( SuccessfullyMeasuredSlice == true );
|
|
|
|
BreakSize.Y = BreakSizeWithoutTrailingWhitespace.Y = MaxAboveBaseline + MaxBelowBaseline;
|
|
|
|
FBreakCandidate BreakCandidate;
|
|
BreakCandidate.ActualSize = BreakSize;
|
|
BreakCandidate.TrimmedSize = BreakSizeWithoutTrailingWhitespace;
|
|
BreakCandidate.ActualRange = FTextRange( PreviousBreak, CurrentBreak );
|
|
BreakCandidate.TrimmedRange = FTextRange( PreviousBreak, WhitespaceStopIndex );
|
|
BreakCandidate.FirstTrailingWhitespaceCharWidth = FirstTrailingWhitespaceCharWidth;
|
|
BreakCandidate.MaxAboveBaseline = MaxAboveBaseline;
|
|
BreakCandidate.MaxBelowBaseline = MaxBelowBaseline;
|
|
BreakCandidate.Kerning = Kerning;
|
|
|
|
#if TEXT_LAYOUT_DEBUG
|
|
BreakCandidate.DebugSlice = FString( BreakCandidate.Range.EndIndex - BreakCandidate.Range.BeginIndex, (**Line.Text) + BreakCandidate.Range.BeginIndex );
|
|
#endif
|
|
|
|
return BreakCandidate;
|
|
}
|
|
|
|
void FTextLayout::CreateLineViewBlocks( int32 LineModelIndex, const int32 StopIndex, const float WrappedLineWidth, int32& OutRunIndex, int32& OutRendererIndex, int32& OutPreviousBlockEnd, TArray< TSharedRef< ILayoutBlock > >& OutSoftLine )
|
|
{
|
|
const FLineModel& LineModel = LineModels[ LineModelIndex ];
|
|
|
|
const FRunTextContext RunTextContext(TextShapingMethod, LineModel.TextBaseDirection, LineModel.ShapedTextCache);
|
|
|
|
int16 MaxAboveBaseline = 0;
|
|
int16 MaxBelowBaseline = 0;
|
|
|
|
int32 CurrentLineBegin = OutPreviousBlockEnd;
|
|
if (OutRunIndex < LineModel.Runs.Num())
|
|
{
|
|
CurrentLineBegin = FMath::Max(CurrentLineBegin, LineModel.Runs[OutRunIndex].GetTextRange().BeginIndex);
|
|
}
|
|
|
|
int32 CurrentLineEnd = StopIndex;
|
|
if (CurrentLineEnd == INDEX_NONE)
|
|
{
|
|
CurrentLineEnd = (LineModel.Runs.Num() > 0) ? LineModel.Runs.Last().GetTextRange().EndIndex : 0;
|
|
}
|
|
|
|
// KerningOnly shaping implies LTR only text, so we can skip the bidirectional detection and splitting
|
|
TextBiDi::ETextDirection LineTextDirection = TextBiDi::ETextDirection::LeftToRight;
|
|
TArray<TextBiDi::FTextDirectionInfo> TextDirectionInfos;
|
|
if (TextShapingMethod != ETextShapingMethod::KerningOnly)
|
|
{
|
|
// The bidirectional text detection tells us the correct order for the blocks of text with regard to the base direction of the current line
|
|
LineTextDirection = TextBiDiDetection->ComputeTextDirection(**LineModel.Text, CurrentLineBegin, CurrentLineEnd - CurrentLineBegin, LineModel.TextBaseDirection, TextDirectionInfos);
|
|
}
|
|
|
|
// Ensure there is at least one directional block. This can happen when using KerningOnly shaping (since we skip the bidirectional detection), or for empty strings that are run through the bidirectional detection.
|
|
if (TextDirectionInfos.Num() == 0)
|
|
{
|
|
TextBiDi::FTextDirectionInfo TextDirectionInfo;
|
|
TextDirectionInfo.StartIndex = CurrentLineBegin;
|
|
TextDirectionInfo.Length = CurrentLineEnd - CurrentLineBegin;
|
|
TextDirectionInfo.TextDirection = TextBiDi::ETextDirection::LeftToRight;
|
|
TextDirectionInfos.Add(MoveTemp(TextDirectionInfo));
|
|
}
|
|
|
|
// We always add the runs to the line in ascending index order, so re-order a copy of the text direction data so that we can iterate it forwards by ascending index
|
|
// We'll re-sort the line into the correct visual order once we've finished generating the blocks
|
|
int32 CurrentSortedTextDirectionInfoIndex = 0;
|
|
TArray<TextBiDi::FTextDirectionInfo> SortedTextDirectionInfos = TextDirectionInfos;
|
|
SortedTextDirectionInfos.Sort([](const TextBiDi::FTextDirectionInfo& InFirst, const TextBiDi::FTextDirectionInfo& InSecond) -> bool
|
|
{
|
|
return InFirst.StartIndex < InSecond.StartIndex;
|
|
});
|
|
|
|
for (; OutRunIndex < LineModel.Runs.Num(); )
|
|
{
|
|
const FRunModel& Run = LineModel.Runs[ OutRunIndex ];
|
|
const FTextRange RunRange = Run.GetTextRange();
|
|
|
|
int32 BlockBeginIndex = FMath::Max( OutPreviousBlockEnd, RunRange.BeginIndex );
|
|
int32 BlockStopIndex = RunRange.EndIndex;
|
|
|
|
// Blocks can only contain text with the same reading direction
|
|
TextBiDi::ETextDirection BlockTextDirection = TextBiDi::ETextDirection::LeftToRight;
|
|
int32 CurrentTextDirectionStopIndex = 0;
|
|
if (CurrentSortedTextDirectionInfoIndex < SortedTextDirectionInfos.Num())
|
|
{
|
|
const TextBiDi::FTextDirectionInfo& CurrentTextDirectionInfo = SortedTextDirectionInfos[CurrentSortedTextDirectionInfoIndex];
|
|
CurrentTextDirectionStopIndex = CurrentTextDirectionInfo.StartIndex + CurrentTextDirectionInfo.Length;
|
|
|
|
check(BlockBeginIndex >= CurrentTextDirectionInfo.StartIndex);
|
|
|
|
BlockStopIndex = FMath::Min(BlockStopIndex, CurrentTextDirectionStopIndex);
|
|
BlockTextDirection = CurrentTextDirectionInfo.TextDirection;
|
|
}
|
|
|
|
TSharedPtr< IRunRenderer > BlockRenderer = nullptr;
|
|
|
|
if ( OutRendererIndex != INDEX_NONE )
|
|
{
|
|
// Grab the currently active renderer
|
|
const FTextRunRenderer& Renderer = LineModel.RunRenderers[OutRendererIndex];
|
|
|
|
// Check to see if the last block was rendered with the same renderer
|
|
if ( OutPreviousBlockEnd >= Renderer.Range.BeginIndex )
|
|
{
|
|
//If the renderer ends before our directional run...
|
|
if ( Renderer.Range.EndIndex <= BlockStopIndex )
|
|
{
|
|
// Adjust the stopping point of the block to be the end of the renderer range,
|
|
// since highlights need their own block segments
|
|
BlockStopIndex = Renderer.Range.EndIndex;
|
|
BlockRenderer = Renderer.Renderer;
|
|
}
|
|
else
|
|
{
|
|
// This whole run is encompassed by the renderer
|
|
BlockRenderer = Renderer.Renderer;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Does the renderer range begin before our directional run ends?
|
|
if ( Renderer.Range.BeginIndex <= BlockStopIndex )
|
|
{
|
|
// then adjust the current block stopping point to just before the renderer range begins,
|
|
// since renderers need their own block segments
|
|
BlockStopIndex = Renderer.Range.BeginIndex;
|
|
BlockRenderer = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (StopIndex != INDEX_NONE)
|
|
{
|
|
BlockStopIndex = FMath::Min(StopIndex, BlockStopIndex);
|
|
}
|
|
|
|
// Have we reached the end of this bidirectional block?
|
|
if (BlockStopIndex == CurrentTextDirectionStopIndex)
|
|
{
|
|
++CurrentSortedTextDirectionInfoIndex;
|
|
}
|
|
|
|
const bool IsLastBlock = BlockStopIndex == StopIndex;
|
|
|
|
check( BlockBeginIndex <= BlockStopIndex );
|
|
|
|
// Add the new block
|
|
{
|
|
FBlockDefinition BlockDefine;
|
|
BlockDefine.ActualRange = FTextRange(BlockBeginIndex, BlockStopIndex);
|
|
BlockDefine.Renderer = BlockRenderer;
|
|
|
|
OutSoftLine.Add( Run.CreateBlock( BlockDefine, Scale, FLayoutBlockTextContext(RunTextContext, BlockTextDirection) ) );
|
|
OutPreviousBlockEnd = BlockStopIndex;
|
|
}
|
|
|
|
// Get the baseline and flip it's sign; Baselines are generally negative
|
|
const int16 Baseline = -(Run.GetBaseLine( Scale ));
|
|
|
|
// For the height of the slice we need to take into account the largest value below and above the baseline and add those together
|
|
MaxAboveBaseline = FMath::Max( MaxAboveBaseline, (int16)( Run.GetMaxHeight( Scale ) - Baseline ) );
|
|
MaxBelowBaseline = FMath::Max( MaxBelowBaseline, Baseline );
|
|
|
|
if ( BlockStopIndex == RunRange.EndIndex )
|
|
{
|
|
++OutRunIndex;
|
|
}
|
|
|
|
if ( OutRendererIndex != INDEX_NONE && BlockStopIndex == LineModel.RunRenderers[ OutRendererIndex ].Range.EndIndex )
|
|
{
|
|
++OutRendererIndex;
|
|
|
|
if ( OutRendererIndex >= LineModel.RunRenderers.Num() )
|
|
{
|
|
OutRendererIndex = INDEX_NONE;
|
|
}
|
|
}
|
|
|
|
if ( IsLastBlock )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
FVector2D LineSize( ForceInitToZero );
|
|
|
|
// Use a negative scroll offset since positive scrolling moves things negatively in screen space
|
|
FVector2D CurrentOffset(-ScrollOffset.X, TextLayoutSize.Height - ScrollOffset.Y);
|
|
|
|
if ( OutSoftLine.Num() > 0 )
|
|
{
|
|
// Re-order the blocks based on their visual direction
|
|
if (OutSoftLine.Num() > 1 && LineTextDirection != TextBiDi::ETextDirection::LeftToRight)
|
|
{
|
|
TArray<TSharedRef<ILayoutBlock>> VisualSoftLine;
|
|
VisualSoftLine.Reserve(OutSoftLine.Num());
|
|
|
|
TArray<TSharedRef<ILayoutBlock>> CurrentVisualSoftLine;
|
|
for (const TextBiDi::FTextDirectionInfo& VisualTextDirectionInfo : TextDirectionInfos)
|
|
{
|
|
const int32 VisualTextEndIndex = VisualTextDirectionInfo.StartIndex + VisualTextDirectionInfo.Length;
|
|
|
|
for (int32 CurrentBlockStartIndex = VisualTextDirectionInfo.StartIndex; CurrentBlockStartIndex < VisualTextEndIndex; )
|
|
{
|
|
const TSharedRef<ILayoutBlock>* FoundLineBlock = OutSoftLine.FindByPredicate([&](const TSharedRef<ILayoutBlock>& InLineBlock) -> bool
|
|
{
|
|
return !InLineBlock->GetTextRange().IsEmpty() && InLineBlock->GetTextRange().BeginIndex == CurrentBlockStartIndex;
|
|
});
|
|
|
|
check(FoundLineBlock);
|
|
|
|
const TSharedRef<ILayoutBlock>& FoundLineBlockRef = *FoundLineBlock;
|
|
if (VisualTextDirectionInfo.TextDirection == TextBiDi::ETextDirection::LeftToRight)
|
|
{
|
|
CurrentVisualSoftLine.Add(FoundLineBlockRef);
|
|
}
|
|
else
|
|
{
|
|
CurrentVisualSoftLine.Insert(FoundLineBlockRef, 0);
|
|
}
|
|
CurrentBlockStartIndex = FoundLineBlockRef->GetTextRange().EndIndex;
|
|
}
|
|
|
|
VisualSoftLine.Append(MoveTemp(CurrentVisualSoftLine));
|
|
CurrentVisualSoftLine.Reset();
|
|
}
|
|
|
|
OutSoftLine = MoveTemp(VisualSoftLine);
|
|
}
|
|
|
|
float CurrentHorizontalPos = 0.0f;
|
|
for (int32 Index = 0; Index < OutSoftLine.Num(); Index++)
|
|
{
|
|
const TSharedRef< ILayoutBlock > Block = OutSoftLine[ Index ];
|
|
const TSharedRef< IRun > Run = Block->GetRun();
|
|
|
|
const int16 BlockBaseline = Run->GetBaseLine(Scale);
|
|
const int16 VerticalOffset = MaxAboveBaseline - Block->GetSize().Y - BlockBaseline;
|
|
const int8 BlockKerning = Run->GetKerning(Block->GetTextRange().BeginIndex, Scale, RunTextContext);
|
|
|
|
Block->SetLocationOffset(FVector2D(CurrentOffset.X + CurrentHorizontalPos + BlockKerning, CurrentOffset.Y + VerticalOffset));
|
|
|
|
CurrentHorizontalPos += Block->GetSize().X;
|
|
}
|
|
|
|
const float UnscaleLineHeight = MaxAboveBaseline + MaxBelowBaseline;
|
|
|
|
LineSize.X = CurrentHorizontalPos;
|
|
LineSize.Y = UnscaleLineHeight * LineHeightPercentage;
|
|
|
|
// Calculate the range for this line, taking into account the fact that text may be flowing right-to-left, so the bounds may be inverted
|
|
const FTextRange& FirstBlockRange = OutSoftLine[0]->GetTextRange();
|
|
const FTextRange& LastBlockRange = OutSoftLine.Last()->GetTextRange();
|
|
const FTextRange LineViewRange = FTextRange(FMath::Min(FirstBlockRange.BeginIndex, LastBlockRange.BeginIndex), FMath::Max(FirstBlockRange.EndIndex, LastBlockRange.EndIndex));
|
|
|
|
FTextLayout::FLineView LineView;
|
|
LineView.Offset = CurrentOffset;
|
|
LineView.Size = LineSize;
|
|
LineView.TextSize = FVector2D(CurrentHorizontalPos, UnscaleLineHeight);
|
|
LineView.Range = LineViewRange;
|
|
LineView.TextBaseDirection = LineModel.TextBaseDirection;
|
|
LineView.ModelIndex = LineModelIndex;
|
|
LineView.Blocks.Append( OutSoftLine );
|
|
|
|
LineViews.Add( LineView );
|
|
}
|
|
|
|
TextLayoutSize.DrawWidth = FMath::Max( TextLayoutSize.DrawWidth, LineSize.X ); // DrawWidth is the size of the longest line + the Margin
|
|
TextLayoutSize.WrappedWidth = FMath::Max( TextLayoutSize.WrappedWidth, (StopIndex == INDEX_NONE) ? LineSize.X : WrappedLineWidth ); // WrappedWidth is the size of the longest line + the Margin + any trailing whitespace width
|
|
TextLayoutSize.Height += LineSize.Y; // Height is the total height of all lines
|
|
}
|
|
|
|
void FTextLayout::JustifyLayout()
|
|
{
|
|
if ( Justification == ETextJustify::Left && TextFlowDirection == ETextFlowDirection::LeftToRight )
|
|
{
|
|
return;
|
|
}
|
|
|
|
const float LayoutWidthNoMargin = FMath::Max(TextLayoutSize.DrawWidth, ViewSize.X * Scale) - ( Margin.GetTotalSpaceAlong<Orient_Horizontal>() * Scale );
|
|
|
|
for (FLineView& LineView : LineViews)
|
|
{
|
|
// Work out the visual justification to use for this line
|
|
ETextJustify::Type VisualJustification = Justification;
|
|
if ( LineView.TextBaseDirection == TextBiDi::ETextDirection::RightToLeft )
|
|
{
|
|
if ( VisualJustification == ETextJustify::Left )
|
|
{
|
|
VisualJustification = ETextJustify::Right;
|
|
}
|
|
else if ( VisualJustification == ETextJustify::Right )
|
|
{
|
|
VisualJustification = ETextJustify::Left;
|
|
}
|
|
}
|
|
|
|
if ( VisualJustification == ETextJustify::Left )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const float ExtraSpace = LayoutWidthNoMargin - LineView.Size.X;
|
|
|
|
FVector2D OffsetAdjustment( ForceInitToZero );
|
|
if ( VisualJustification == ETextJustify::Center )
|
|
{
|
|
OffsetAdjustment = FVector2D( ExtraSpace * 0.5f, 0 );
|
|
}
|
|
else if ( VisualJustification == ETextJustify::Right )
|
|
{
|
|
OffsetAdjustment = FVector2D( ExtraSpace, 0 );
|
|
}
|
|
|
|
LineView.Offset += OffsetAdjustment;
|
|
|
|
for (const TSharedRef< ILayoutBlock >& Block : LineView.Blocks)
|
|
{
|
|
Block->SetLocationOffset( Block->GetLocationOffset() + OffsetAdjustment );
|
|
}
|
|
}
|
|
}
|
|
|
|
float FTextLayout::GetWrappingDrawWidth() const
|
|
{
|
|
check( WrappingWidth >= 0 );
|
|
return FMath::Max( 0.01f, ( WrappingWidth - Margin.GetTotalSpaceAlong<Orient_Horizontal>() ) * Scale );
|
|
}
|
|
|
|
void FTextLayout::FlowLayout()
|
|
{
|
|
const float WrappingDrawWidth = GetWrappingDrawWidth();
|
|
|
|
TArray< TSharedRef< ILayoutBlock > > SoftLine;
|
|
for (int32 LineModelIndex = 0; LineModelIndex < LineModels.Num(); LineModelIndex++)
|
|
{
|
|
FLineModel& LineModel = LineModels[ LineModelIndex ];
|
|
CalculateLineTextDirection(LineModel);
|
|
FlushLineTextShapingCache(LineModel);
|
|
CreateLineWrappingCache(LineModel);
|
|
|
|
FlowLineLayout(LineModelIndex, WrappingDrawWidth, SoftLine);
|
|
}
|
|
}
|
|
|
|
void FTextLayout::MarginLayout()
|
|
{
|
|
// Add on the margins to the layout size
|
|
const float MarginWidth = Margin.GetTotalSpaceAlong<Orient_Horizontal>() * Scale;
|
|
const float MarginHeight = Margin.GetTotalSpaceAlong<Orient_Vertical>() * Scale;
|
|
TextLayoutSize.DrawWidth += MarginWidth;
|
|
TextLayoutSize.WrappedWidth += MarginWidth;
|
|
TextLayoutSize.Height += MarginHeight;
|
|
|
|
// Adjust the lines to be offset
|
|
FVector2D OffsetAdjustment = FVector2D(Margin.Left, Margin.Top) * Scale;
|
|
for (FLineView& LineView : LineViews)
|
|
{
|
|
LineView.Offset += OffsetAdjustment;
|
|
|
|
for (const TSharedRef< ILayoutBlock >& Block : LineView.Blocks)
|
|
{
|
|
Block->SetLocationOffset( Block->GetLocationOffset() + OffsetAdjustment );
|
|
}
|
|
}
|
|
}
|
|
|
|
void FTextLayout::FlowLineLayout(const int32 LineModelIndex, const float WrappingDrawWidth, TArray<TSharedRef<ILayoutBlock>>& SoftLine)
|
|
{
|
|
const FLineModel& LineModel = LineModels[ LineModelIndex ];
|
|
|
|
float CurrentWidth = 0.0f;
|
|
int32 CurrentRunIndex = 0;
|
|
int32 PreviousBlockEnd = 0;
|
|
|
|
int32 CurrentRendererIndex = 0;
|
|
if ( CurrentRendererIndex >= LineModel.RunRenderers.Num() )
|
|
{
|
|
CurrentRendererIndex = INDEX_NONE;
|
|
}
|
|
|
|
const bool IsWrapping = WrappingWidth > 0.0f;
|
|
|
|
// if the Line doesn't have any BreakCandidates, or we're not wrapping text
|
|
if (!IsWrapping || LineModel.BreakCandidates.Num() == 0 )
|
|
{
|
|
//Then iterate over all of it's runs
|
|
CreateLineViewBlocks( LineModelIndex, INDEX_NONE, 0.0f, /*OUT*/CurrentRunIndex, /*OUT*/CurrentRendererIndex, /*OUT*/PreviousBlockEnd, SoftLine );
|
|
check( CurrentRunIndex == LineModel.Runs.Num() );
|
|
CurrentWidth = 0;
|
|
SoftLine.Reset();
|
|
}
|
|
else
|
|
{
|
|
for (int32 BreakIndex = 0; BreakIndex < LineModel.BreakCandidates.Num(); BreakIndex++)
|
|
{
|
|
const FBreakCandidate& Break = LineModel.BreakCandidates[ BreakIndex ];
|
|
|
|
const bool IsLastBreak = BreakIndex + 1 == LineModel.BreakCandidates.Num();
|
|
const bool IsFirstBreakOnSoftLine = CurrentWidth == 0;
|
|
const uint8 Kerning = ( IsFirstBreakOnSoftLine ) ? Break.Kerning : 0;
|
|
const bool BreakDoesFit = !IsWrapping || CurrentWidth + Break.ActualSize.X + Kerning <= WrappingDrawWidth;
|
|
|
|
if ( !BreakDoesFit || IsLastBreak )
|
|
{
|
|
const bool BreakWithoutTrailingWhitespaceDoesFit = !IsWrapping || CurrentWidth + Break.TrimmedSize.X + Kerning <= WrappingDrawWidth;
|
|
const bool IsFirstBreak = BreakIndex == 0;
|
|
|
|
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 && !IsLastBreak )
|
|
{
|
|
// This break has trailing whitespace
|
|
WrappedLineWidth += ( FinalBreakOnSoftLine.TrimmedSize.X + FinalBreakOnSoftLine.FirstTrailingWhitespaceCharWidth );
|
|
}
|
|
else
|
|
{
|
|
// This break is either longer than the wrapping point or the last break on this line, so make sure and clamp the line size to the given wrapping width
|
|
WrappedLineWidth += FinalBreakOnSoftLine.ActualSize.X;
|
|
WrappedLineWidth = FMath::Min(WrappedLineWidth, WrappingDrawWidth);
|
|
}
|
|
|
|
CreateLineViewBlocks( LineModelIndex, FinalBreakOnSoftLine.ActualRange.EndIndex, WrappedLineWidth, /*OUT*/CurrentRunIndex, /*OUT*/CurrentRendererIndex, /*OUT*/PreviousBlockEnd, SoftLine );
|
|
|
|
if ( CurrentRunIndex < LineModel.Runs.Num() && FinalBreakOnSoftLine.ActualRange.EndIndex == LineModel.Runs[ CurrentRunIndex ].GetTextRange().EndIndex )
|
|
{
|
|
++CurrentRunIndex;
|
|
}
|
|
|
|
PreviousBlockEnd = FinalBreakOnSoftLine.ActualRange.EndIndex;
|
|
|
|
CurrentWidth = 0;
|
|
SoftLine.Reset();
|
|
}
|
|
else
|
|
{
|
|
CurrentWidth += Break.ActualSize.X;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FTextLayout::FlowHighlights()
|
|
{
|
|
struct FVisualHighlightBoundary
|
|
{
|
|
FVisualHighlightBoundary()
|
|
: BlockIndex(INDEX_NONE)
|
|
, RangeIndex(INDEX_NONE)
|
|
{
|
|
}
|
|
|
|
int32 BlockIndex;
|
|
int32 RangeIndex;
|
|
};
|
|
|
|
// FlowLayout must have been called first
|
|
check(!(DirtyFlags & ETextLayoutDirtyState::Layout));
|
|
|
|
for (FLineView& LineView : LineViews)
|
|
{
|
|
LineView.UnderlayHighlights.Empty();
|
|
LineView.OverlayHighlights.Empty();
|
|
|
|
FLineModel& LineModel = LineModels[LineView.ModelIndex];
|
|
|
|
const FRunTextContext RunTextContext(TextShapingMethod, LineModel.TextBaseDirection, LineModel.ShapedTextCache);
|
|
|
|
// Insert each highlighter into every line view that's within its range, either as an underlay, or as an overlay
|
|
for (FTextLineHighlight& LineHighlight : LineModel.LineHighlights)
|
|
{
|
|
if (LineHighlight.LineIndex != LineView.ModelIndex)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const bool bIsHighlightInRange = LineView.Range.InclusiveContains(LineHighlight.Range.BeginIndex) && LineView.Range.InclusiveContains(LineHighlight.Range.EndIndex);
|
|
if(!bIsHighlightInRange)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto AppendLineViewHighlight = [&](const FLineViewHighlight& InLineViewHighlight)
|
|
{
|
|
if (LineHighlight.ZOrder < 0)
|
|
{
|
|
LineView.UnderlayHighlights.Add(InLineViewHighlight);
|
|
}
|
|
else
|
|
{
|
|
LineView.OverlayHighlights.Add(InLineViewHighlight);
|
|
}
|
|
};
|
|
|
|
FLineViewHighlight LineViewHighlight;
|
|
LineViewHighlight.OffsetX = 0.0f;
|
|
LineViewHighlight.Width = 0.0f;
|
|
LineViewHighlight.Highlighter = LineHighlight.Highlighter;
|
|
|
|
// Find the start and end block for this highlight
|
|
FVisualHighlightBoundary VisualHighlightStart;
|
|
FVisualHighlightBoundary VisualHighlightEnd;
|
|
for (int32 CurrentBlockIndex = 0; CurrentBlockIndex < LineView.Blocks.Num(); ++CurrentBlockIndex)
|
|
{
|
|
const TSharedRef<ILayoutBlock>& Block = LineView.Blocks[CurrentBlockIndex];
|
|
const FTextRange& BlockTextRange = Block->GetTextRange();
|
|
|
|
if (BlockTextRange.InclusiveContains(LineHighlight.Range.BeginIndex))
|
|
{
|
|
VisualHighlightStart.BlockIndex = CurrentBlockIndex;
|
|
VisualHighlightStart.RangeIndex = LineHighlight.Range.BeginIndex;
|
|
}
|
|
|
|
if (BlockTextRange.InclusiveContains(LineHighlight.Range.EndIndex))
|
|
{
|
|
VisualHighlightEnd.BlockIndex = CurrentBlockIndex;
|
|
VisualHighlightEnd.RangeIndex = LineHighlight.Range.EndIndex;
|
|
}
|
|
|
|
if (VisualHighlightStart.BlockIndex != INDEX_NONE && VisualHighlightEnd.BlockIndex != INDEX_NONE)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
check(VisualHighlightStart.BlockIndex != INDEX_NONE && VisualHighlightEnd.BlockIndex != INDEX_NONE);
|
|
|
|
// Right-to-left text can lead to VisualHighlightStart being after VisualHighlightEnd, we need to flip them if this has happened since we walk the blocks to highlight left-to-right
|
|
if (VisualHighlightStart.BlockIndex > VisualHighlightEnd.BlockIndex)
|
|
{
|
|
Swap(VisualHighlightStart, VisualHighlightEnd);
|
|
}
|
|
|
|
// Measure the blocks up to the start of this highlight to get the correct start offset
|
|
for (int32 CurrentBlockIndex = 0; CurrentBlockIndex < VisualHighlightStart.BlockIndex; ++CurrentBlockIndex)
|
|
{
|
|
const TSharedRef<ILayoutBlock>& Block = LineView.Blocks[CurrentBlockIndex];
|
|
LineViewHighlight.OffsetX += Block->GetSize().X;
|
|
}
|
|
|
|
const TSharedRef<ILayoutBlock>& StartBlock = LineView.Blocks[VisualHighlightStart.BlockIndex];
|
|
const TSharedRef<ILayoutBlock>& EndBlock = LineView.Blocks[VisualHighlightEnd.BlockIndex];
|
|
const bool bIsSingleBlock = VisualHighlightStart.BlockIndex == VisualHighlightEnd.BlockIndex;
|
|
const bool bIsVisuallyContiguous = StartBlock->GetTextContext().TextDirection == EndBlock->GetTextContext().TextDirection;
|
|
|
|
float RunningBlockOffset = LineViewHighlight.OffsetX;
|
|
|
|
// Handle any offset and size from the start block
|
|
{
|
|
RunningBlockOffset += StartBlock->GetSize().X;
|
|
|
|
const FTextRange& BlockTextRange = StartBlock->GetTextRange();
|
|
const TSharedRef<IRun> Run = StartBlock->GetRun();
|
|
|
|
// The width always includes size of the intersecting text
|
|
const FTextRange IntersectedRange = BlockTextRange.Intersect(LineHighlight.Range);
|
|
if (!IntersectedRange.IsEmpty())
|
|
{
|
|
LineViewHighlight.Width += Run->Measure(IntersectedRange.BeginIndex, IntersectedRange.EndIndex, Scale, RunTextContext).X;
|
|
}
|
|
|
|
// In left-to-right text, the space before the start of the text is added as an offset
|
|
// In right-to-left text, the space after the end of the text (which is visually on the left) is added as an offset
|
|
if (StartBlock->GetTextContext().TextDirection == TextBiDi::ETextDirection::LeftToRight)
|
|
{
|
|
LineViewHighlight.OffsetX += Run->Measure(BlockTextRange.BeginIndex, IntersectedRange.BeginIndex, Scale, RunTextContext).X;
|
|
}
|
|
else
|
|
{
|
|
LineViewHighlight.OffsetX += Run->Measure(IntersectedRange.EndIndex, BlockTextRange.EndIndex, Scale, RunTextContext).X;
|
|
}
|
|
}
|
|
|
|
// If we need to deal with other blocks, then we also need to deal with splitting the highlight for non-contiguous text
|
|
if (!bIsSingleBlock)
|
|
{
|
|
if (!bIsVisuallyContiguous)
|
|
{
|
|
// Append the block for the first part of the highlight and reset it to deal with the middle part
|
|
AppendLineViewHighlight(LineViewHighlight);
|
|
|
|
LineViewHighlight.OffsetX = RunningBlockOffset;
|
|
LineViewHighlight.Width = 0.0f;
|
|
}
|
|
|
|
// Measure the blocks under this highlight to get the correct size
|
|
for (int32 CurrentBlockIndex = VisualHighlightStart.BlockIndex + 1; CurrentBlockIndex < VisualHighlightEnd.BlockIndex; ++CurrentBlockIndex)
|
|
{
|
|
const TSharedRef<ILayoutBlock>& Block = LineView.Blocks[CurrentBlockIndex];
|
|
LineViewHighlight.Width += Block->GetSize().X;
|
|
RunningBlockOffset += Block->GetSize().X;
|
|
}
|
|
|
|
if (!bIsVisuallyContiguous)
|
|
{
|
|
// Append the block for the middle part of the highlight (if any) and reset it to deal with the end part
|
|
if (LineViewHighlight.Width > 0.0f)
|
|
{
|
|
AppendLineViewHighlight(LineViewHighlight);
|
|
}
|
|
|
|
LineViewHighlight.OffsetX = RunningBlockOffset;
|
|
LineViewHighlight.Width = 0.0f;
|
|
}
|
|
|
|
// Handle any size from the end block
|
|
{
|
|
const FTextRange& BlockTextRange = EndBlock->GetTextRange();
|
|
const TSharedRef<IRun> Run = EndBlock->GetRun();
|
|
|
|
// The width always includes size of the intersecting text
|
|
const FTextRange IntersectedRange = BlockTextRange.Intersect(LineHighlight.Range);
|
|
if (!IntersectedRange.IsEmpty())
|
|
{
|
|
LineViewHighlight.Width += Run->Measure(IntersectedRange.BeginIndex, IntersectedRange.EndIndex, Scale, RunTextContext).X;
|
|
}
|
|
|
|
// Right-to-left text in a left-to-right text flow will need to apply an offset to compensate for the fact that the blocks flow left-to-right, but the text within them flows right-to-left
|
|
// When the text flow is right-to-left, this is naturally dealt with by the block re-ordering
|
|
if (EndBlock->GetTextContext().TextDirection == TextBiDi::ETextDirection::RightToLeft && EndBlock->GetTextContext().BaseDirection == TextBiDi::ETextDirection::LeftToRight)
|
|
{
|
|
LineViewHighlight.OffsetX += Run->Measure(IntersectedRange.EndIndex, BlockTextRange.EndIndex, Scale, RunTextContext).X;
|
|
}
|
|
}
|
|
}
|
|
|
|
AppendLineViewHighlight(LineViewHighlight);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FTextLayout::EndLayout()
|
|
{
|
|
for (FLineModel& LineModel : LineModels)
|
|
{
|
|
EndLineLayout(LineModel);
|
|
}
|
|
}
|
|
|
|
void FTextLayout::EndLineLayout(FLineModel& LineModel)
|
|
{
|
|
for (FRunModel& RunModel : LineModel.Runs)
|
|
{
|
|
RunModel.EndLayout();
|
|
}
|
|
}
|
|
|
|
void FTextLayout::BeginLayout()
|
|
{
|
|
for (FLineModel& LineModel : LineModels)
|
|
{
|
|
BeginLineLayout(LineModel);
|
|
}
|
|
}
|
|
|
|
void FTextLayout::BeginLineLayout(FLineModel& LineModel)
|
|
{
|
|
for (FRunModel& RunModel : LineModel.Runs)
|
|
{
|
|
RunModel.BeginLayout();
|
|
}
|
|
}
|
|
|
|
void FTextLayout::ClearView()
|
|
{
|
|
TextLayoutSize = FTextLayoutSize();
|
|
LineViews.Empty();
|
|
}
|
|
|
|
void FTextLayout::CalculateTextDirection()
|
|
{
|
|
for (FLineModel& LineModel : LineModels)
|
|
{
|
|
CalculateLineTextDirection(LineModel);
|
|
}
|
|
}
|
|
|
|
void FTextLayout::CalculateLineTextDirection(FLineModel& LineModel)
|
|
{
|
|
if (!(LineModel.DirtyFlags & ELineModelDirtyState::TextBaseDirection))
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch(TextFlowDirection)
|
|
{
|
|
case ETextFlowDirection::Auto:
|
|
// KerningOnly shaping implies LTR only text, so we can skip the text direction detection
|
|
LineModel.TextBaseDirection = (TextShapingMethod == ETextShapingMethod::KerningOnly) ? TextBiDi::ETextDirection::LeftToRight : TextBiDi::ComputeBaseDirection(*LineModel.Text);
|
|
break;
|
|
case ETextFlowDirection::LeftToRight:
|
|
LineModel.TextBaseDirection = TextBiDi::ETextDirection::LeftToRight;
|
|
break;
|
|
case ETextFlowDirection::RightToLeft:
|
|
LineModel.TextBaseDirection = TextBiDi::ETextDirection::RightToLeft;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
LineModel.DirtyFlags &= ~ELineModelDirtyState::TextBaseDirection;
|
|
}
|
|
|
|
void FTextLayout::CreateWrappingCache()
|
|
{
|
|
const bool IsWrapping = WrappingWidth > 0.0f;
|
|
if (!IsWrapping)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (FLineModel& LineModel : LineModels)
|
|
{
|
|
CreateLineWrappingCache(LineModel);
|
|
}
|
|
}
|
|
|
|
void FTextLayout::CreateLineWrappingCache(FLineModel& LineModel)
|
|
{
|
|
if (!(LineModel.DirtyFlags & ELineModelDirtyState::WrappingInformation))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
LineModel.BreakCandidates.Empty();
|
|
LineModel.DirtyFlags &= ~ELineModelDirtyState::WrappingInformation;
|
|
|
|
for (int32 RunIndex = 0; RunIndex < LineModel.Runs.Num(); RunIndex++)
|
|
{
|
|
LineModel.Runs[ RunIndex ].ClearCache();
|
|
}
|
|
|
|
LineBreakIterator->SetString( **LineModel.Text );
|
|
|
|
int32 PreviousBreak = 0;
|
|
int32 CurrentBreak = 0;
|
|
int32 CurrentRunIndex = 0;
|
|
|
|
while( ( CurrentBreak = LineBreakIterator->MoveToNext() ) != INDEX_NONE )
|
|
{
|
|
LineModel.BreakCandidates.Add( CreateBreakCandidate(/*OUT*/CurrentRunIndex, LineModel, PreviousBreak, CurrentBreak) );
|
|
PreviousBreak = CurrentBreak;
|
|
}
|
|
|
|
LineBreakIterator->ClearString();
|
|
}
|
|
|
|
void FTextLayout::FlushTextShapingCache()
|
|
{
|
|
for (FLineModel& LineModel : LineModels)
|
|
{
|
|
FlushLineTextShapingCache(LineModel);
|
|
}
|
|
}
|
|
|
|
void FTextLayout::FlushLineTextShapingCache(FLineModel& LineModel)
|
|
{
|
|
if (!(LineModel.DirtyFlags & ELineModelDirtyState::ShapingCache))
|
|
{
|
|
return;
|
|
}
|
|
|
|
LineModel.ShapedTextCache->Clear();
|
|
LineModel.DirtyFlags &= ~ELineModelDirtyState::ShapingCache;
|
|
}
|
|
|
|
void FTextLayout::DirtyAllLineModels(const ELineModelDirtyState::Flags InDirtyFlags)
|
|
{
|
|
for (FLineModel& LineModel : LineModels)
|
|
{
|
|
LineModel.DirtyFlags |= InDirtyFlags;
|
|
}
|
|
}
|
|
|
|
FTextLayout::FTextLayout()
|
|
: LineModels()
|
|
, LineViews()
|
|
, DirtyFlags( ETextLayoutDirtyState::None )
|
|
, TextShapingMethod( GetDefaultTextShapingMethod() )
|
|
, TextFlowDirection( GetDefaultTextFlowDirection() )
|
|
, Scale( 1.0f )
|
|
, WrappingWidth( 0 )
|
|
, Margin()
|
|
, Justification( ETextJustify::Left )
|
|
, LineHeightPercentage( 1.0f )
|
|
, TextLayoutSize()
|
|
, ViewSize( ForceInitToZero )
|
|
, ScrollOffset( ForceInitToZero )
|
|
, LineBreakIterator() // Initialized in FTextLayout::CreateWrappingCache if no custom iterator is provided
|
|
, WordBreakIterator(FBreakIterator::CreateWordBreakIterator())
|
|
, TextBiDiDetection(TextBiDi::CreateTextBiDi())
|
|
{
|
|
|
|
}
|
|
|
|
void FTextLayout::UpdateIfNeeded()
|
|
{
|
|
const bool bHasChangedLayout = !!(DirtyFlags & ETextLayoutDirtyState::Layout);
|
|
const bool bHasChangedHighlights = !!(DirtyFlags & ETextLayoutDirtyState::Highlights);
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
void FTextLayout::UpdateLayout()
|
|
{
|
|
ClearView();
|
|
BeginLayout();
|
|
|
|
FlowLayout();
|
|
JustifyLayout();
|
|
MarginLayout();
|
|
|
|
EndLayout();
|
|
|
|
DirtyFlags &= ~ETextLayoutDirtyState::Layout;
|
|
}
|
|
|
|
void FTextLayout::UpdateHighlights()
|
|
{
|
|
FlowHighlights();
|
|
|
|
DirtyFlags &= ~ETextLayoutDirtyState::Highlights;
|
|
}
|
|
|
|
void FTextLayout::DirtyRunLayout(const TSharedRef<const IRun>& Run)
|
|
{
|
|
for (int32 LineModelIndex = 0; LineModelIndex < LineModels.Num(); LineModelIndex++)
|
|
{
|
|
FLineModel& LineModel = LineModels[LineModelIndex];
|
|
|
|
if (!(LineModel.DirtyFlags & ELineModelDirtyState::WrappingInformation))
|
|
{
|
|
for (int32 RunIndex = 0; RunIndex < LineModel.Runs.Num(); RunIndex++)
|
|
{
|
|
if (LineModel.Runs[RunIndex].GetRun() == Run)
|
|
{
|
|
LineModel.Runs[RunIndex].ClearCache();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
}
|
|
|
|
void FTextLayout::DirtyLayout()
|
|
{
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
|
|
// Clear out the entire cache so it gets regenerated on the text call to FlowLayout
|
|
DirtyAllLineModels(ELineModelDirtyState::All);
|
|
}
|
|
|
|
bool FTextLayout::IsLayoutDirty() const
|
|
{
|
|
return !!(DirtyFlags & ETextLayoutDirtyState::Layout);
|
|
}
|
|
|
|
void FTextLayout::ClearRunRenderers()
|
|
{
|
|
for (int32 Index = 0; Index < LineModels.Num(); Index++)
|
|
{
|
|
if (LineModels[ Index ].RunRenderers.Num() )
|
|
{
|
|
LineModels[ Index ].RunRenderers.Empty();
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FTextLayout::SetRunRenderers( const TArray< FTextRunRenderer >& Renderers )
|
|
{
|
|
ClearRunRenderers();
|
|
|
|
for (int32 Index = 0; Index < Renderers.Num(); Index++)
|
|
{
|
|
AddRunRenderer( Renderers[ Index ] );
|
|
}
|
|
}
|
|
|
|
void FTextLayout::AddRunRenderer( const FTextRunRenderer& Renderer )
|
|
{
|
|
checkf( LineModels.IsValidIndex( Renderer.LineIndex ), TEXT("Renderers must be for a valid Line Index") );
|
|
|
|
FLineModel& LineModel = LineModels[ Renderer.LineIndex ];
|
|
|
|
// Renderers needs to be in order and not overlap
|
|
bool bWasInserted = false;
|
|
for (int32 Index = 0; Index < LineModel.RunRenderers.Num() && !bWasInserted; Index++)
|
|
{
|
|
if ( LineModel.RunRenderers[ Index ].Range.BeginIndex > Renderer.Range.BeginIndex )
|
|
{
|
|
checkf( Index == 0 || LineModel.RunRenderers[ Index - 1 ].Range.EndIndex <= Renderer.Range.BeginIndex, TEXT("Renderers cannot overlap") );
|
|
LineModel.RunRenderers.Insert( Renderer, Index - 1 );
|
|
bWasInserted = true;
|
|
}
|
|
else if ( LineModel.RunRenderers[ Index ].Range.EndIndex > Renderer.Range.EndIndex )
|
|
{
|
|
checkf( LineModel.RunRenderers[ Index ].Range.BeginIndex >= Renderer.Range.EndIndex, TEXT("Renderers cannot overlap") );
|
|
LineModel.RunRenderers.Insert( Renderer, Index - 1 );
|
|
bWasInserted = true;
|
|
}
|
|
}
|
|
|
|
if ( !bWasInserted )
|
|
{
|
|
LineModel.RunRenderers.Add( Renderer );
|
|
}
|
|
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
}
|
|
|
|
void FTextLayout::ClearLineHighlights()
|
|
{
|
|
for (int32 Index = 0; Index < LineModels.Num(); Index++)
|
|
{
|
|
if (LineModels[ Index ].LineHighlights.Num())
|
|
{
|
|
LineModels[ Index ].LineHighlights.Empty();
|
|
DirtyFlags |= ETextLayoutDirtyState::Highlights;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FTextLayout::SetLineHighlights( const TArray< FTextLineHighlight >& Highlights )
|
|
{
|
|
ClearLineHighlights();
|
|
|
|
for (int32 Index = 0; Index < Highlights.Num(); Index++)
|
|
{
|
|
AddLineHighlight( Highlights[ Index ] );
|
|
}
|
|
}
|
|
|
|
void FTextLayout::AddLineHighlight( const FTextLineHighlight& Highlight )
|
|
{
|
|
checkf( LineModels.IsValidIndex( Highlight.LineIndex ), TEXT("Highlights must be for a valid Line Index") );
|
|
checkf( Highlight.ZOrder, TEXT("The highlight Z-order must be <0 to create an underlay, or >0 to create an overlay") );
|
|
|
|
FLineModel& LineModel = LineModels[ Highlight.LineIndex ];
|
|
|
|
// Try and maintain a stable sorted z-order - highlights with the same z-order should just render in the order they were added
|
|
bool bWasInserted = false;
|
|
for (int32 Index = 0; Index < LineModel.LineHighlights.Num() && !bWasInserted; Index++)
|
|
{
|
|
if ( LineModel.LineHighlights[ Index ].ZOrder > Highlight.ZOrder )
|
|
{
|
|
LineModel.LineHighlights.Insert( Highlight, Index - 1 );
|
|
bWasInserted = true;
|
|
}
|
|
}
|
|
|
|
if ( !bWasInserted )
|
|
{
|
|
LineModel.LineHighlights.Add( Highlight );
|
|
}
|
|
|
|
DirtyFlags |= ETextLayoutDirtyState::Highlights;
|
|
}
|
|
|
|
FTextLocation FTextLayout::GetTextLocationAt( const FLineView& LineView, const FVector2D& Relative, ETextHitPoint* const OutHitPoint ) const
|
|
{
|
|
for (int32 BlockIndex = 0; BlockIndex < LineView.Blocks.Num(); BlockIndex++)
|
|
{
|
|
const TSharedRef< ILayoutBlock >& Block = LineView.Blocks[ BlockIndex ];
|
|
const int32 TextIndex = Block->GetRun()->GetTextIndexAt(Block, FVector2D(Relative.X, Block->GetLocationOffset().Y), Scale, OutHitPoint);
|
|
|
|
if ( TextIndex == INDEX_NONE )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return FTextLocation( LineView.ModelIndex, TextIndex );
|
|
}
|
|
|
|
const FLineModel& LineModel = LineModels[ LineView.ModelIndex ];
|
|
const int32 LineTextLength = LineModel.Text->Len();
|
|
if ( LineTextLength == 0 || !LineView.Blocks.Num() )
|
|
{
|
|
if(OutHitPoint)
|
|
{
|
|
*OutHitPoint = ETextHitPoint::WithinText;
|
|
}
|
|
return FTextLocation( LineView.ModelIndex, 0 );
|
|
}
|
|
else if (Relative.X < LineView.Blocks[0]->GetLocationOffset().X)
|
|
{
|
|
const auto& Block = LineView.Blocks[0];
|
|
const FTextRange BlockRange = Block->GetTextRange();
|
|
const FLayoutBlockTextContext BlockContext = Block->GetTextContext();
|
|
if (BlockContext.TextDirection == TextBiDi::ETextDirection::LeftToRight)
|
|
{
|
|
if(OutHitPoint)
|
|
{
|
|
*OutHitPoint = ETextHitPoint::LeftGutter;
|
|
}
|
|
return FTextLocation(LineView.ModelIndex, BlockRange.BeginIndex);
|
|
}
|
|
else
|
|
{
|
|
if(OutHitPoint)
|
|
{
|
|
*OutHitPoint = ETextHitPoint::RightGutter;
|
|
}
|
|
return FTextLocation(LineView.ModelIndex, BlockRange.EndIndex);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const auto& Block = LineView.Blocks.Last();
|
|
const FTextRange BlockRange = Block->GetTextRange();
|
|
const FLayoutBlockTextContext BlockContext = Block->GetTextContext();
|
|
if (BlockContext.TextDirection == TextBiDi::ETextDirection::LeftToRight)
|
|
{
|
|
if(OutHitPoint)
|
|
{
|
|
*OutHitPoint = ETextHitPoint::RightGutter;
|
|
}
|
|
return FTextLocation(LineView.ModelIndex, BlockRange.EndIndex);
|
|
}
|
|
else
|
|
{
|
|
if(OutHitPoint)
|
|
{
|
|
*OutHitPoint = ETextHitPoint::LeftGutter;
|
|
}
|
|
return FTextLocation(LineView.ModelIndex, BlockRange.BeginIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 FTextLayout::GetLineViewIndexForTextLocation(const TArray< FTextLayout::FLineView >& InLineViews, const FTextLocation& Location, const bool bPerformInclusiveBoundsCheck) const
|
|
{
|
|
const int32 LineModelIndex = Location.GetLineIndex();
|
|
const int32 Offset = Location.GetOffset();
|
|
|
|
const FLineModel& LineModel = LineModels[LineModelIndex];
|
|
for(int32 Index = 0; Index < InLineViews.Num(); Index++)
|
|
{
|
|
const FTextLayout::FLineView& LineView = InLineViews[Index];
|
|
|
|
if(LineView.ModelIndex == LineModelIndex)
|
|
{
|
|
// Simple case where we're either the start of, or are contained within, the line view
|
|
if(Offset == 0 || LineModel.Text->IsEmpty() || LineView.Range.Contains(Offset))
|
|
{
|
|
return Index;
|
|
}
|
|
|
|
// If we're the last line, then we also need to test for the end index being part of the range
|
|
const bool bIsLastLineForModel = Index == (InLineViews.Num() - 1) || InLineViews[Index + 1].ModelIndex != LineModelIndex;
|
|
if((bIsLastLineForModel || bPerformInclusiveBoundsCheck) && LineView.Range.EndIndex == Offset)
|
|
{
|
|
return Index;
|
|
}
|
|
}
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
FTextLocation FTextLayout::GetTextLocationAt( const FVector2D& Relative, ETextHitPoint* const OutHitPoint ) const
|
|
{
|
|
// Early out if we have no LineViews
|
|
if (LineViews.Num() == 0)
|
|
{
|
|
return FTextLocation(0,0);
|
|
}
|
|
|
|
// Iterate until we find a LineView that is below our expected Y location
|
|
int32 ViewIndex;
|
|
for (ViewIndex = 0; ViewIndex < LineViews.Num(); ViewIndex++)
|
|
{
|
|
const FLineView& LineView = LineViews[ ViewIndex ];
|
|
|
|
if (LineView.Offset.Y > Relative.Y)
|
|
{
|
|
// Set the ViewIndex back to the previous line, but not lower than the first line
|
|
ViewIndex = FMath::Max( 0, ViewIndex - 1 );
|
|
break;
|
|
}
|
|
}
|
|
|
|
//if none of the lines are below our expected Y location then...
|
|
if (ViewIndex >= LineViews.Num())
|
|
{
|
|
// just use the very last line
|
|
ViewIndex = LineViews.Num() - 1;
|
|
const FLineView& LineView = LineViews[ ViewIndex ];
|
|
return GetTextLocationAt( LineView, Relative, OutHitPoint );
|
|
}
|
|
else
|
|
{
|
|
// if the current LineView does not encapsulate our expected Y location then jump to the next LineView
|
|
// if we aren't already at the last LineView
|
|
const FLineView& LineView = LineViews[ ViewIndex ];
|
|
if ((LineView.Offset.Y + LineView.Size.Y) < Relative.Y && ViewIndex < LineViews.Num() - 1)
|
|
{
|
|
++ViewIndex;
|
|
}
|
|
}
|
|
|
|
const FLineView& LineView = LineViews[ ViewIndex ];
|
|
return GetTextLocationAt( LineView, FVector2D(Relative.X, LineView.Offset.Y), OutHitPoint );
|
|
}
|
|
|
|
FVector2D FTextLayout::GetLocationAt( const FTextLocation& Location, const bool bPerformInclusiveBoundsCheck ) const
|
|
{
|
|
const int32 LineModelIndex = Location.GetLineIndex();
|
|
const int32 Offset = Location.GetOffset();
|
|
|
|
// Find the LineView which encapsulates the location's offset
|
|
const int32 LineViewIndex = GetLineViewIndexForTextLocation( LineViews, Location, bPerformInclusiveBoundsCheck );
|
|
|
|
// if we failed to find a LineView for the location, early out
|
|
if ( !LineViews.IsValidIndex( LineViewIndex ) )
|
|
{
|
|
return FVector2D( ForceInitToZero );
|
|
}
|
|
|
|
const FTextLayout::FLineView& LineView = LineViews[ LineViewIndex ];
|
|
|
|
//Iterate over the LineView's blocks...
|
|
for (int32 BlockIndex = 0; BlockIndex < LineView.Blocks.Num(); BlockIndex++)
|
|
{
|
|
const TSharedRef< ILayoutBlock >& Block = LineView.Blocks[ BlockIndex ];
|
|
const FTextRange& BlockRange = Block->GetTextRange();
|
|
|
|
//if the block's range contains the specified locations offset...
|
|
if (BlockRange.InclusiveContains(Offset))
|
|
{
|
|
// Ask the block for the exact screen location
|
|
const FVector2D ScreenLocation = Block->GetRun()->GetLocationAt( Block, Offset - BlockRange.BeginIndex, Scale );
|
|
|
|
// if the block was unable to provide a location continue iterating
|
|
if ( ScreenLocation.IsZero() )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return ScreenLocation;
|
|
}
|
|
}
|
|
|
|
// Failed to find the screen location
|
|
return FVector2D( ForceInitToZero );
|
|
}
|
|
|
|
bool FTextLayout::InsertAt(const FTextLocation& Location, TCHAR Character)
|
|
{
|
|
const int32 InsertLocation = Location.GetOffset();
|
|
const int32 LineIndex = Location.GetLineIndex();
|
|
|
|
if (!LineModels.IsValidIndex(LineIndex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FLineModel& LineModel = LineModels[LineIndex];
|
|
|
|
LineModel.Text->InsertAt(InsertLocation, Character);
|
|
LineModel.DirtyFlags |= ELineModelDirtyState::All;
|
|
|
|
bool RunIsAfterInsertLocation = false;
|
|
for (int32 RunIndex = 0; RunIndex < LineModel.Runs.Num(); RunIndex++)
|
|
{
|
|
FRunModel& RunModel = LineModel.Runs[ RunIndex ];
|
|
const FTextRange& RunRange = RunModel.GetTextRange();
|
|
|
|
const bool bIsLastRun = RunIndex == LineModel.Runs.Num() - 1;
|
|
if (RunRange.Contains(InsertLocation) || (bIsLastRun && !RunIsAfterInsertLocation))
|
|
{
|
|
check(RunIsAfterInsertLocation == false);
|
|
RunIsAfterInsertLocation = true;
|
|
|
|
if ((RunModel.GetRun()->GetRunAttributes() & ERunAttributes::SupportsText) != ERunAttributes::None)
|
|
{
|
|
RunModel.SetTextRange( FTextRange( RunRange.BeginIndex, RunRange.EndIndex + 1 ) );
|
|
}
|
|
else
|
|
{
|
|
// Non-text runs are supposed to have a single dummy character in them
|
|
check(RunRange.Len() == 1);
|
|
|
|
// This run doesn't support text, so we need to insert a new text run before or after the current run depending on the insertion point
|
|
const bool bIsInsertingToTheLeft = InsertLocation == RunRange.BeginIndex;
|
|
if (bIsInsertingToTheLeft)
|
|
{
|
|
// Insert the new text run to the left of the non-text run
|
|
TSharedRef<IRun> NewTextRun = CreateDefaultTextRun( LineModel.Text, FTextRange( RunRange.BeginIndex, RunRange.BeginIndex + 1 ) );
|
|
RunModel.SetTextRange( FTextRange( RunRange.BeginIndex + 1, RunRange.EndIndex + 1 ) );
|
|
LineModel.Runs.Insert( NewTextRun, RunIndex++ );
|
|
}
|
|
else
|
|
{
|
|
// Insert the new text run to the right of the non-text run
|
|
TSharedRef<IRun> NewTextRun = CreateDefaultTextRun( LineModel.Text, FTextRange( RunRange.EndIndex, RunRange.EndIndex + 1 ) );
|
|
LineModel.Runs.Insert( NewTextRun, ++RunIndex );
|
|
}
|
|
}
|
|
}
|
|
else if (RunIsAfterInsertLocation)
|
|
{
|
|
FTextRange NewRange = RunRange;
|
|
NewRange.Offset(1);
|
|
RunModel.SetTextRange(NewRange);
|
|
}
|
|
}
|
|
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
return true;
|
|
}
|
|
|
|
bool FTextLayout::InsertAt(const FTextLocation& Location, const FString& Text)
|
|
{
|
|
const int32 InsertLocation = Location.GetOffset();
|
|
const int32 LineIndex = Location.GetLineIndex();
|
|
|
|
if (!LineModels.IsValidIndex(LineIndex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FLineModel& LineModel = LineModels[LineIndex];
|
|
|
|
LineModel.Text->InsertAt(InsertLocation, Text);
|
|
LineModel.DirtyFlags |= ELineModelDirtyState::All;
|
|
|
|
bool RunIsAfterInsertLocation = false;
|
|
for (int32 RunIndex = 0; RunIndex < LineModel.Runs.Num(); RunIndex++)
|
|
{
|
|
FRunModel& RunModel = LineModel.Runs[ RunIndex ];
|
|
const FTextRange& RunRange = RunModel.GetTextRange();
|
|
|
|
const bool bIsLastRun = RunIndex == LineModel.Runs.Num() - 1;
|
|
if (RunRange.Contains(InsertLocation) || (bIsLastRun && !RunIsAfterInsertLocation))
|
|
{
|
|
check(RunIsAfterInsertLocation == false);
|
|
RunIsAfterInsertLocation = true;
|
|
|
|
if ((RunModel.GetRun()->GetRunAttributes() & ERunAttributes::SupportsText) != ERunAttributes::None)
|
|
{
|
|
RunModel.SetTextRange( FTextRange( RunRange.BeginIndex, RunRange.EndIndex + Text.Len() ) );
|
|
}
|
|
else
|
|
{
|
|
// Non-text runs are supposed to have a single dummy character in them
|
|
check(RunRange.Len() == 1);
|
|
|
|
// This run doesn't support text, so we need to insert a new text run before or after the current run depending on the insertion point
|
|
const bool bIsInsertingToTheLeft = InsertLocation == RunRange.BeginIndex;
|
|
if (bIsInsertingToTheLeft)
|
|
{
|
|
// Insert the new text run to the left of the non-text run
|
|
TSharedRef<IRun> NewTextRun = CreateDefaultTextRun( LineModel.Text, FTextRange( RunRange.BeginIndex, RunRange.BeginIndex + Text.Len() ) );
|
|
RunModel.SetTextRange( FTextRange( RunRange.BeginIndex + 1, RunRange.EndIndex + Text.Len() ) );
|
|
LineModel.Runs.Insert( NewTextRun, RunIndex++ );
|
|
}
|
|
else
|
|
{
|
|
// Insert the new text run to the right of the non-text run
|
|
TSharedRef<IRun> NewTextRun = CreateDefaultTextRun( LineModel.Text, FTextRange( RunRange.EndIndex, RunRange.EndIndex + Text.Len() ) );
|
|
LineModel.Runs.Insert( NewTextRun, ++RunIndex );
|
|
}
|
|
}
|
|
}
|
|
else if (RunIsAfterInsertLocation)
|
|
{
|
|
FTextRange NewRange = RunRange;
|
|
NewRange.Offset(Text.Len());
|
|
RunModel.SetTextRange(NewRange);
|
|
}
|
|
}
|
|
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
return true;
|
|
}
|
|
|
|
bool FTextLayout::InsertAt(const FTextLocation& Location, TSharedRef<IRun> InRun, const bool bAlwaysKeepRightRun)
|
|
{
|
|
const int32 InsertLocation = Location.GetOffset();
|
|
const int32 LineIndex = Location.GetLineIndex();
|
|
|
|
if (!LineModels.IsValidIndex(LineIndex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FLineModel& LineModel = LineModels[LineIndex];
|
|
|
|
FString NewRunText;
|
|
InRun->AppendTextTo(NewRunText);
|
|
|
|
LineModel.Text->InsertAt(InsertLocation, NewRunText);
|
|
LineModel.DirtyFlags |= ELineModelDirtyState::All;
|
|
|
|
bool RunIsAfterInsertLocation = false;
|
|
for (int32 RunIndex = 0; RunIndex < LineModel.Runs.Num(); RunIndex++)
|
|
{
|
|
const TSharedRef<IRun> Run = LineModel.Runs[RunIndex].GetRun();
|
|
const FTextRange& RunRange = Run->GetTextRange();
|
|
|
|
const bool bIsLastRun = RunIndex == LineModel.Runs.Num() - 1;
|
|
if (RunRange.Contains(InsertLocation) || (bIsLastRun && !RunIsAfterInsertLocation))
|
|
{
|
|
check(RunIsAfterInsertLocation == false);
|
|
RunIsAfterInsertLocation = true;
|
|
|
|
const int32 InsertLocationEnd = InsertLocation + NewRunText.Len();
|
|
|
|
// This run contains the insertion point, so we need to split it
|
|
TSharedPtr<IRun> LeftRun;
|
|
TSharedPtr<IRun> RightRun;
|
|
if ((Run->GetRunAttributes() & ERunAttributes::SupportsText) != ERunAttributes::None)
|
|
{
|
|
LeftRun = Run->Clone();
|
|
LeftRun->SetTextRange(FTextRange(RunRange.BeginIndex, InsertLocation));
|
|
|
|
RightRun = Run;
|
|
RightRun->SetTextRange(FTextRange(InsertLocationEnd, RunRange.EndIndex + NewRunText.Len()));
|
|
}
|
|
else
|
|
{
|
|
// Non-text runs are supposed to have a single dummy character in them
|
|
check(RunRange.Len() == 1);
|
|
|
|
// This run doesn't support text, so we need to insert a new text run before or after the current run depending on the insertion point
|
|
const bool bIsInsertingToTheLeft = InsertLocation == RunRange.BeginIndex;
|
|
if (bIsInsertingToTheLeft)
|
|
{
|
|
// Insert the new text run to the left of the non-text run
|
|
LeftRun = CreateDefaultTextRun(LineModel.Text, FTextRange(RunRange.BeginIndex, InsertLocation));
|
|
|
|
RightRun = Run;
|
|
RightRun->SetTextRange(FTextRange(InsertLocationEnd, RunRange.EndIndex + NewRunText.Len()));
|
|
}
|
|
else
|
|
{
|
|
// Insert the new text run to the right of the non-text run
|
|
LeftRun = Run;
|
|
|
|
RightRun = CreateDefaultTextRun(LineModel.Text, FTextRange(InsertLocationEnd, RunRange.EndIndex + NewRunText.Len()));
|
|
}
|
|
}
|
|
|
|
InRun->Move(LineModel.Text, FTextRange(InsertLocation, InsertLocationEnd));
|
|
|
|
// Remove the old run (it may get re-added again as the right hand run)
|
|
LineModel.Runs.RemoveAt(RunIndex--);
|
|
|
|
// Insert the new runs at the correct place, and then skip over these new array entries
|
|
const bool LeftRunHasText = !LeftRun->GetTextRange().IsEmpty();
|
|
const bool RightRunHasText = !RightRun->GetTextRange().IsEmpty();
|
|
if (LeftRunHasText)
|
|
{
|
|
LineModel.Runs.Insert(LeftRun.ToSharedRef(), ++RunIndex);
|
|
}
|
|
LineModel.Runs.Insert(InRun, ++RunIndex);
|
|
if (RightRunHasText || bAlwaysKeepRightRun)
|
|
{
|
|
LineModel.Runs.Insert(RightRun.ToSharedRef(), ++RunIndex);
|
|
}
|
|
}
|
|
else if (RunIsAfterInsertLocation)
|
|
{
|
|
FTextRange NewRange = RunRange;
|
|
NewRange.Offset(NewRunText.Len());
|
|
Run->SetTextRange(NewRange);
|
|
}
|
|
}
|
|
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
return true;
|
|
}
|
|
|
|
bool FTextLayout::JoinLineWithNextLine(int32 LineIndex)
|
|
{
|
|
if (!LineModels.IsValidIndex(LineIndex) || !LineModels.IsValidIndex(LineIndex + 1))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
//Grab both lines we are supposed to join
|
|
FLineModel& LineModel = LineModels[LineIndex];
|
|
FLineModel& NextLineModel = LineModels[LineIndex + 1];
|
|
|
|
//If the next line is empty we'll just remove it
|
|
if (NextLineModel.Text->Len() == 0)
|
|
{
|
|
return RemoveLine(LineIndex + 1);
|
|
}
|
|
|
|
const int32 LineLength = LineModel.Text->Len();
|
|
|
|
//Append the next line to the current line
|
|
LineModel.Text->InsertAt(LineLength, NextLineModel.Text.Get());
|
|
|
|
//Dirty the current line
|
|
LineModel.DirtyFlags |= ELineModelDirtyState::All;
|
|
|
|
//Iterate through all of the next lines runs and bring them over to the current line
|
|
for (int32 RunIndex = 0; RunIndex < NextLineModel.Runs.Num(); RunIndex++)
|
|
{
|
|
const TSharedRef<IRun> Run = NextLineModel.Runs[RunIndex].GetRun();
|
|
FTextRange NewRange = Run->GetTextRange();
|
|
|
|
if (!NewRange.IsEmpty())
|
|
{
|
|
NewRange.Offset(LineLength);
|
|
|
|
Run->Move(LineModel.Text, NewRange);
|
|
LineModel.Runs.Add(FRunModel(Run));
|
|
}
|
|
}
|
|
|
|
//Remove the next line from the list of line models
|
|
LineModels.RemoveAt(LineIndex + 1);
|
|
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
return true;
|
|
}
|
|
|
|
bool FTextLayout::SplitLineAt(const FTextLocation& Location)
|
|
{
|
|
int32 BreakLocation = Location.GetOffset();
|
|
int32 LineIndex = Location.GetLineIndex();
|
|
|
|
if (!LineModels.IsValidIndex(LineIndex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FLineModel& LineModel = LineModels[LineIndex];
|
|
|
|
FLineModel LeftLineModel(MakeShareable(new FString(BreakLocation, **LineModel.Text)));
|
|
FLineModel RightLineModel(MakeShareable(new FString(LineModel.Text->Len() - BreakLocation, **LineModel.Text + BreakLocation)));
|
|
|
|
check(LeftLineModel.Text->Len() == BreakLocation);
|
|
|
|
bool RunIsToTheLeftOfTheBreakLocation = true;
|
|
for (int32 RunIndex = 0; RunIndex < LineModel.Runs.Num(); RunIndex++)
|
|
{
|
|
const TSharedRef<IRun> Run = LineModel.Runs[RunIndex].GetRun();
|
|
const FTextRange& RunRange = Run->GetTextRange();
|
|
|
|
const bool bIsLastRun = RunIndex == LineModel.Runs.Num() - 1;
|
|
if (RunRange.Contains(BreakLocation) || (bIsLastRun && RunIsToTheLeftOfTheBreakLocation))
|
|
{
|
|
check(RunIsToTheLeftOfTheBreakLocation == true);
|
|
RunIsToTheLeftOfTheBreakLocation = false;
|
|
|
|
TSharedPtr<IRun> LeftRun;
|
|
TSharedPtr<IRun> RightRun;
|
|
if ((Run->GetRunAttributes() & ERunAttributes::SupportsText) != ERunAttributes::None)
|
|
{
|
|
LeftRun = Run->Clone();
|
|
LeftRun->Move(LeftLineModel.Text, FTextRange(RunRange.BeginIndex, LeftLineModel.Text->Len()));
|
|
|
|
RightRun = Run;
|
|
RightRun->Move(RightLineModel.Text, FTextRange(0, RunRange.EndIndex - LeftLineModel.Text->Len()));
|
|
}
|
|
else
|
|
{
|
|
// Non-text runs are supposed to have a single dummy character in them
|
|
check(RunRange.Len() == 1);
|
|
|
|
// This run doesn't support text, so we need to insert a new text run before or after the current run depending on the insertion point
|
|
const bool bIsInsertingToTheLeft = BreakLocation == RunRange.BeginIndex;
|
|
if (bIsInsertingToTheLeft)
|
|
{
|
|
// Insert the new text run to the left of the non-text run
|
|
LeftRun = CreateDefaultTextRun(LeftLineModel.Text, FTextRange(RunRange.BeginIndex, LeftLineModel.Text->Len()));
|
|
|
|
RightRun = Run;
|
|
RightRun->Move(RightLineModel.Text, FTextRange(0, RunRange.EndIndex - LeftLineModel.Text->Len()));
|
|
}
|
|
else
|
|
{
|
|
// Insert the new text run to the right of the non-text run
|
|
LeftRun = Run;
|
|
LeftRun->Move(LeftLineModel.Text, FTextRange(RunRange.BeginIndex, LeftLineModel.Text->Len()));
|
|
|
|
RightRun = CreateDefaultTextRun(RightLineModel.Text, FTextRange(0, RunRange.EndIndex - LeftLineModel.Text->Len()));
|
|
}
|
|
}
|
|
|
|
LeftLineModel.Runs.Add(FRunModel(LeftRun.ToSharedRef()));
|
|
RightLineModel.Runs.Add(FRunModel(RightRun.ToSharedRef()));
|
|
}
|
|
else if (RunIsToTheLeftOfTheBreakLocation)
|
|
{
|
|
Run->Move(LeftLineModel.Text, RunRange);
|
|
LeftLineModel.Runs.Add(FRunModel(Run));
|
|
}
|
|
else
|
|
{
|
|
// This run is after the break, so adjust the range to match that of RHS of the split (RightLineModel)
|
|
// We can do this by subtracting the LeftLineModel text size, since that's the LHS of the split
|
|
FTextRange NewRange = RunRange;
|
|
NewRange.Offset(-LeftLineModel.Text->Len());
|
|
|
|
Run->Move(RightLineModel.Text, NewRange);
|
|
RightLineModel.Runs.Add(FRunModel(Run));
|
|
}
|
|
}
|
|
|
|
LineModels.RemoveAt(LineIndex);
|
|
LineModels.Insert(LeftLineModel, LineIndex);
|
|
LineModels.Insert(RightLineModel, LineIndex + 1);
|
|
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
return true;
|
|
}
|
|
|
|
bool FTextLayout::RemoveAt( const FTextLocation& Location, int32 Count )
|
|
{
|
|
int32 RemoveLocation = Location.GetOffset();
|
|
int32 LineIndex = Location.GetLineIndex();
|
|
|
|
if (!LineModels.IsValidIndex(LineIndex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FLineModel& LineModel = LineModels[LineIndex];
|
|
|
|
//Make sure we aren't trying to remove more characters then we have
|
|
Count = RemoveLocation + Count > LineModel.Text->Len() ? Count - ((RemoveLocation + Count) - LineModel.Text->Len()) : Count;
|
|
|
|
if (Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LineModel.Text->RemoveAt(RemoveLocation, Count);
|
|
LineModel.DirtyFlags |= ELineModelDirtyState::All;
|
|
|
|
const FTextRange RemoveTextRange(RemoveLocation, RemoveLocation + Count);
|
|
for (int32 RunIndex = LineModel.Runs.Num() - 1; RunIndex >= 0; --RunIndex)
|
|
{
|
|
FRunModel& RunModel = LineModel.Runs[RunIndex];
|
|
const FTextRange RunRange = RunModel.GetTextRange();
|
|
|
|
const FTextRange IntersectedRangeToRemove = RunRange.Intersect(RemoveTextRange);
|
|
if (IntersectedRangeToRemove.IsEmpty() && RunRange.BeginIndex >= RemoveTextRange.EndIndex)
|
|
{
|
|
// The whole run is contained to the right of the removal range, just adjust its range by the amount of text that was removed
|
|
FTextRange NewRange = RunRange;
|
|
NewRange.Offset(-Count);
|
|
RunModel.SetTextRange(NewRange);
|
|
}
|
|
else if (!IntersectedRangeToRemove.IsEmpty())
|
|
{
|
|
const int32 RunLength = RunRange.Len();
|
|
const int32 IntersectedLength = IntersectedRangeToRemove.Len();
|
|
if (RunLength == IntersectedLength)
|
|
{
|
|
// The text for this entire run has been removed, so remove this run
|
|
LineModel.Runs.RemoveAt(RunIndex);
|
|
|
|
// Every line needs at least one run - if we just removed the last run for this line, add a new default text run with a zero range
|
|
if (LineModel.Runs.Num() == 0)
|
|
{
|
|
TSharedRef<IRun> NewTextRun = CreateDefaultTextRun(LineModel.Text, FTextRange(0, 0));
|
|
LineModel.Runs.Add(NewTextRun);
|
|
}
|
|
}
|
|
else if (RunRange.BeginIndex > RemoveTextRange.BeginIndex)
|
|
{
|
|
// Some of this run has been removed, and this run is the right hand part of the removal
|
|
// So we need to adjust the range so that we start at the removal point since the text has been removed from the beginning of this run
|
|
const FTextRange NewRange(RemoveTextRange.BeginIndex, RunRange.EndIndex - Count);
|
|
check(!NewRange.IsEmpty());
|
|
RunModel.SetTextRange(NewRange);
|
|
}
|
|
else
|
|
{
|
|
// Some of this run has been removed, and this run is the left hand part of the removal
|
|
// So we need to adjust the range by the amount of text that has been removed from the end of this run
|
|
const FTextRange NewRange(RunRange.BeginIndex, RunRange.EndIndex - IntersectedLength);
|
|
check(!NewRange.IsEmpty());
|
|
RunModel.SetTextRange(NewRange);
|
|
}
|
|
|
|
if (RunRange.BeginIndex <= RemoveTextRange.BeginIndex)
|
|
{
|
|
// break since we don't need to process the runs to the left of the removal point
|
|
break;
|
|
}
|
|
}
|
|
else if(IntersectedRangeToRemove.IsEmpty() && RunRange.IsEmpty() && RemoveTextRange.Contains(RunRange.BeginIndex) && RemoveTextRange.Contains(RunRange.EndIndex))
|
|
{
|
|
// empty run that was inside our removed range, safe to remove
|
|
LineModel.Runs.RemoveAt(RunIndex);
|
|
}
|
|
}
|
|
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
return true;
|
|
}
|
|
|
|
bool FTextLayout::RemoveLine(int32 LineIndex)
|
|
{
|
|
if (!LineModels.IsValidIndex(LineIndex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LineModels.RemoveAt(LineIndex);
|
|
|
|
// If our layout is clean, then we can remove this line immediately (and efficiently)
|
|
// If our layout is dirty, then we might as well wait as the next UpdateLayout call will remove it
|
|
if (!(DirtyFlags & ETextLayoutDirtyState::Layout))
|
|
{
|
|
//Lots of room for additional optimization
|
|
float OffsetAdjustment = 0;
|
|
|
|
for (int32 ViewIndex = 0; ViewIndex < LineViews.Num(); ViewIndex++)
|
|
{
|
|
FLineView& LineView = LineViews[ViewIndex];
|
|
|
|
if (LineView.ModelIndex == LineIndex)
|
|
{
|
|
if (ViewIndex - 1 <= 0)
|
|
{
|
|
OffsetAdjustment += LineView.Offset.Y;
|
|
}
|
|
else
|
|
{
|
|
//Since the offsets are not relative to other lines, if we aren't removing the top line then
|
|
//we don't aggregate the offset from any previous removals as we'd be double counting.
|
|
OffsetAdjustment = (LineView.Offset.Y - LineViews[ViewIndex - 1].Offset.Y);
|
|
}
|
|
|
|
LineViews.RemoveAt(ViewIndex);
|
|
--ViewIndex;
|
|
}
|
|
else if (LineView.ModelIndex > LineIndex)
|
|
{
|
|
//We've removed a line model so update the LineView indices
|
|
--LineView.ModelIndex;
|
|
LineView.Offset.Y -= OffsetAdjustment;
|
|
|
|
for (const TSharedRef< ILayoutBlock >& Block : LineView.Blocks)
|
|
{
|
|
FVector2D BlockOffset = Block->GetLocationOffset();
|
|
BlockOffset.Y -= OffsetAdjustment;
|
|
Block->SetLocationOffset(BlockOffset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FTextLayout::AddLine( const TSharedRef< FString >& Text, const TArray< TSharedRef< IRun > >& Runs )
|
|
{
|
|
{
|
|
FLineModel LineModel( Text );
|
|
|
|
for (int32 Index = 0; Index < Runs.Num(); Index++)
|
|
{
|
|
LineModel.Runs.Add( FRunModel( Runs[ Index ] ) );
|
|
}
|
|
|
|
LineModels.Add( LineModel );
|
|
}
|
|
|
|
// If our layout is clean, then we can add this new line immediately (and efficiently)
|
|
// If our layout is dirty, then we might as well wait as the next UpdateLayout call will add it
|
|
if (!(DirtyFlags & ETextLayoutDirtyState::Layout))
|
|
{
|
|
const int32 LineModelIndex = LineModels.Num() - 1;
|
|
FLineModel& LineModel = LineModels[LineModelIndex];
|
|
|
|
CalculateLineTextDirection(LineModel);
|
|
FlushLineTextShapingCache(LineModel);
|
|
CreateLineWrappingCache(LineModel);
|
|
|
|
BeginLineLayout(LineModel);
|
|
|
|
TArray<TSharedRef<ILayoutBlock>> SoftLine;
|
|
FlowLineLayout(LineModelIndex, GetWrappingDrawWidth(), SoftLine);
|
|
|
|
// Apply the current margin to the newly added line
|
|
if (LineViews.Num() > 0)
|
|
{
|
|
const FVector2D MarginOffsetAdjustment = FVector2D(Margin.Left, Margin.Top) * Scale;
|
|
|
|
FLineView& LastLineView = LineViews.Last();
|
|
if (LastLineView.ModelIndex == LineModelIndex)
|
|
{
|
|
LastLineView.Offset += MarginOffsetAdjustment;
|
|
}
|
|
|
|
for (const TSharedRef< ILayoutBlock >& Block : SoftLine)
|
|
{
|
|
Block->SetLocationOffset( Block->GetLocationOffset() + MarginOffsetAdjustment );
|
|
}
|
|
}
|
|
|
|
// We need to re-justify all lines, as the new line view(s) added by this line model may have affected everything
|
|
JustifyLayout();
|
|
|
|
EndLineLayout(LineModel);
|
|
}
|
|
}
|
|
|
|
void FTextLayout::ClearLines()
|
|
{
|
|
LineModels.Empty();
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
}
|
|
|
|
bool FTextLayout::IsEmpty() const
|
|
{
|
|
return (LineModels.Num() == 0 || (LineModels.Num() == 1 && LineModels[0].Text->Len() == 0));
|
|
}
|
|
|
|
void FTextLayout::GetAsText(FString& DisplayText, FTextOffsetLocations* const OutTextOffsetLocations) const
|
|
{
|
|
GetAsTextAndOffsets(&DisplayText, OutTextOffsetLocations);
|
|
}
|
|
|
|
void FTextLayout::GetAsText(FText& DisplayText, FTextOffsetLocations* const OutTextOffsetLocations) const
|
|
{
|
|
FString DisplayString;
|
|
GetAsText(DisplayString, OutTextOffsetLocations);
|
|
DisplayText = FText::FromString(DisplayString);
|
|
}
|
|
|
|
void FTextLayout::GetTextOffsetLocations(FTextOffsetLocations& OutTextOffsetLocations) const
|
|
{
|
|
GetAsTextAndOffsets(nullptr, &OutTextOffsetLocations);
|
|
}
|
|
|
|
void FTextLayout::GetAsTextAndOffsets(FString* const OutDisplayText, FTextOffsetLocations* const OutTextOffsetLocations) const
|
|
{
|
|
int32 DisplayTextLength = 0;
|
|
|
|
if (OutTextOffsetLocations)
|
|
{
|
|
OutTextOffsetLocations->OffsetData.Reserve(LineModels.Num());
|
|
}
|
|
|
|
const int32 LineTerminatorLength = FCString::Strlen(LINE_TERMINATOR);
|
|
|
|
for (int32 LineModelIndex = 0; LineModelIndex < LineModels.Num(); LineModelIndex++)
|
|
{
|
|
const FLineModel& LineModel = LineModels[LineModelIndex];
|
|
|
|
// Append line terminator to the end of the previous line
|
|
if (LineModelIndex > 0)
|
|
{
|
|
if (OutDisplayText)
|
|
{
|
|
*OutDisplayText += LINE_TERMINATOR;
|
|
}
|
|
DisplayTextLength += LineTerminatorLength;
|
|
}
|
|
|
|
int32 LineLength = 0;
|
|
for (int32 RunIndex = 0; RunIndex < LineModel.Runs.Num(); RunIndex++)
|
|
{
|
|
const FRunModel& Run = LineModel.Runs[RunIndex];
|
|
if (OutDisplayText)
|
|
{
|
|
Run.AppendTextTo(*OutDisplayText);
|
|
}
|
|
LineLength += Run.GetTextRange().Len();
|
|
}
|
|
|
|
if (OutTextOffsetLocations)
|
|
{
|
|
OutTextOffsetLocations->OffsetData.Add(FTextOffsetLocations::FOffsetEntry(DisplayTextLength, LineLength));
|
|
}
|
|
|
|
DisplayTextLength += LineLength;
|
|
}
|
|
}
|
|
|
|
void GetRangeAsTextFromLine(FString& DisplayText, const FTextLayout::FLineModel& LineModel, const FTextRange& Range)
|
|
{
|
|
for (int32 RunIndex = 0; RunIndex < LineModel.Runs.Num(); RunIndex++)
|
|
{
|
|
const FTextLayout::FRunModel& Run = LineModel.Runs[RunIndex];
|
|
const FTextRange& RunRange = Run.GetTextRange();
|
|
|
|
const FTextRange IntersectRange = RunRange.Intersect(Range);
|
|
|
|
if (!IntersectRange.IsEmpty())
|
|
{
|
|
Run.AppendTextTo(DisplayText, IntersectRange);
|
|
}
|
|
else if (RunRange.BeginIndex > Range.EndIndex)
|
|
{
|
|
//We're past the selection range so we can stop
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FTextLayout::GetSelectionAsText(FString& DisplayText, const FTextSelection& Selection) const
|
|
{
|
|
int32 SelectionBeginningLineIndex = Selection.GetBeginning().GetLineIndex();
|
|
int32 SelectionBeginningLineOffset = Selection.GetBeginning().GetOffset();
|
|
|
|
int32 SelectionEndLineIndex = Selection.GetEnd().GetLineIndex();
|
|
int32 SelectionEndLineOffset = Selection.GetEnd().GetOffset();
|
|
|
|
if (LineModels.IsValidIndex(SelectionBeginningLineIndex) && LineModels.IsValidIndex(SelectionEndLineIndex))
|
|
{
|
|
if (SelectionBeginningLineIndex == SelectionEndLineIndex)
|
|
{
|
|
const FTextRange SelectionRange(SelectionBeginningLineOffset, SelectionEndLineOffset);
|
|
const FLineModel& LineModel = LineModels[SelectionBeginningLineIndex];
|
|
|
|
GetRangeAsTextFromLine(DisplayText, LineModel, SelectionRange);
|
|
}
|
|
else
|
|
{
|
|
for (int32 LineIndex = SelectionBeginningLineIndex; LineIndex <= SelectionEndLineIndex; LineIndex++)
|
|
{
|
|
if (LineIndex == SelectionBeginningLineIndex)
|
|
{
|
|
const FLineModel& LineModel = LineModels[SelectionBeginningLineIndex];
|
|
const FTextRange SelectionRange(SelectionBeginningLineOffset, LineModel.Text->Len());
|
|
|
|
GetRangeAsTextFromLine(DisplayText, LineModel, SelectionRange);
|
|
}
|
|
else if (LineIndex == SelectionEndLineIndex)
|
|
{
|
|
const FLineModel& LineModel = LineModels[SelectionEndLineIndex];
|
|
const FTextRange SelectionRange(0, SelectionEndLineOffset);
|
|
|
|
GetRangeAsTextFromLine(DisplayText, LineModel, SelectionRange);
|
|
}
|
|
else
|
|
{
|
|
const FLineModel& LineModel = LineModels[LineIndex];
|
|
const FTextRange SelectionRange(0, LineModel.Text->Len());
|
|
|
|
GetRangeAsTextFromLine(DisplayText, LineModel, SelectionRange);
|
|
}
|
|
|
|
if (LineIndex != SelectionEndLineIndex)
|
|
{
|
|
DisplayText += LINE_TERMINATOR;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FTextSelection FTextLayout::GetWordAt(const FTextLocation& Location) const
|
|
{
|
|
const int32 LineIndex = Location.GetLineIndex();
|
|
const int32 Offset = Location.GetOffset();
|
|
|
|
if (!LineModels.IsValidIndex(LineIndex))
|
|
{
|
|
return FTextSelection();
|
|
}
|
|
|
|
const FLineModel& LineModel = LineModels[LineIndex];
|
|
|
|
WordBreakIterator->SetString(**LineModel.Text);
|
|
|
|
int32 PreviousBreak = WordBreakIterator->MoveToCandidateAfter(Offset);
|
|
int32 CurrentBreak = 0;
|
|
|
|
while ((CurrentBreak = WordBreakIterator->MoveToPrevious()) != INDEX_NONE)
|
|
{
|
|
bool HasLetter = false;
|
|
for (int32 Index = CurrentBreak; Index < PreviousBreak; Index++)
|
|
{
|
|
if (!FText::IsWhitespace((*LineModel.Text)[Index]))
|
|
{
|
|
HasLetter = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (HasLetter)
|
|
{
|
|
break;
|
|
}
|
|
|
|
PreviousBreak = CurrentBreak;
|
|
}
|
|
|
|
WordBreakIterator->ClearString();
|
|
|
|
if (PreviousBreak == CurrentBreak || CurrentBreak == INDEX_NONE)
|
|
{
|
|
return FTextSelection();
|
|
}
|
|
|
|
return FTextSelection(FTextLocation(LineIndex, CurrentBreak), FTextLocation(LineIndex, PreviousBreak));
|
|
}
|
|
|
|
void FTextLayout::SetVisibleRegion( const FVector2D& InViewSize, const FVector2D& InScrollOffset )
|
|
{
|
|
if (ViewSize != InViewSize)
|
|
{
|
|
ViewSize = InViewSize;
|
|
|
|
if (Justification != ETextJustify::Left)
|
|
{
|
|
// If the view size has changed, we may need to update our positions based on our justification
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
}
|
|
}
|
|
|
|
if (ScrollOffset != InScrollOffset)
|
|
{
|
|
const FVector2D PreviousScrollOffset = ScrollOffset;
|
|
ScrollOffset = InScrollOffset;
|
|
|
|
// Use a negative scroll offset since positive scrolling moves things negatively in screen space
|
|
const FVector2D OffsetAdjustment = -(ScrollOffset - PreviousScrollOffset);
|
|
|
|
for (FLineView& LineView : LineViews)
|
|
{
|
|
LineView.Offset += OffsetAdjustment;
|
|
|
|
for (const TSharedRef< ILayoutBlock >& Block : LineView.Blocks)
|
|
{
|
|
Block->SetLocationOffset( Block->GetLocationOffset() + OffsetAdjustment );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FTextLayout::SetLineBreakIterator( TSharedPtr<IBreakIterator> InLineBreakIterator )
|
|
{
|
|
LineBreakIterator = InLineBreakIterator;
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
|
|
// Changing the line break iterator 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 | ELineModelDirtyState::ShapingCache);
|
|
}
|
|
|
|
void FTextLayout::SetMargin( const FMargin& InMargin )
|
|
{
|
|
if ( Margin == InMargin )
|
|
{
|
|
return;
|
|
}
|
|
|
|
Margin = InMargin;
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
}
|
|
|
|
void FTextLayout::SetScale( float Value )
|
|
{
|
|
if ( Scale == Value )
|
|
{
|
|
return;
|
|
}
|
|
|
|
Scale = Value;
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
|
|
// Changing the scale 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 | ELineModelDirtyState::ShapingCache);
|
|
}
|
|
|
|
void FTextLayout::SetTextShapingMethod( const ETextShapingMethod InTextShapingMethod )
|
|
{
|
|
if ( TextShapingMethod == InTextShapingMethod )
|
|
{
|
|
return;
|
|
}
|
|
|
|
TextShapingMethod = InTextShapingMethod;
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
|
|
// Changing the shaping method will affect the wrapping information for *all lines*
|
|
// Clear out the entire cache so it gets regenerated on the text call to FlowLayout
|
|
// Also clear our the base direction for each line, as the shaping method can affect that
|
|
DirtyAllLineModels(ELineModelDirtyState::WrappingInformation | ELineModelDirtyState::TextBaseDirection | ELineModelDirtyState::ShapingCache);
|
|
}
|
|
|
|
void FTextLayout::SetTextFlowDirection( const ETextFlowDirection InTextFlowDirection )
|
|
{
|
|
if ( TextFlowDirection == InTextFlowDirection )
|
|
{
|
|
return;
|
|
}
|
|
|
|
TextFlowDirection = InTextFlowDirection;
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
|
|
// Changing the flow direction will affect the wrapping information for *all lines*
|
|
// Clear out the entire cache so it gets regenerated on the text call to FlowLayout
|
|
// Also clear our the base direction for each line, as the flow direction can affect that
|
|
DirtyAllLineModels(ELineModelDirtyState::WrappingInformation | ELineModelDirtyState::TextBaseDirection | ELineModelDirtyState::ShapingCache);
|
|
}
|
|
|
|
void FTextLayout::SetJustification( ETextJustify::Type Value )
|
|
{
|
|
if ( Justification == Value )
|
|
{
|
|
return;
|
|
}
|
|
|
|
Justification = Value;
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
}
|
|
|
|
void FTextLayout::SetLineHeightPercentage( float Value )
|
|
{
|
|
if ( LineHeightPercentage != Value )
|
|
{
|
|
LineHeightPercentage = Value;
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
}
|
|
}
|
|
|
|
void FTextLayout::SetWrappingWidth( float Value )
|
|
{
|
|
if ( WrappingWidth != Value )
|
|
{
|
|
WrappingWidth = Value;
|
|
DirtyFlags |= ETextLayoutDirtyState::Layout;
|
|
}
|
|
}
|
|
|
|
FVector2D FTextLayout::GetDrawSize() const
|
|
{
|
|
return TextLayoutSize.GetDrawSize();
|
|
}
|
|
|
|
FVector2D FTextLayout::GetWrappedSize() const
|
|
{
|
|
return TextLayoutSize.GetWrappedSize() * ( 1 / Scale );
|
|
}
|
|
|
|
FVector2D FTextLayout::GetSize() const
|
|
{
|
|
return TextLayoutSize.GetDrawSize() * ( 1 / Scale );
|
|
}
|
|
|
|
FTextLayout::~FTextLayout()
|
|
{
|
|
|
|
}
|
|
|
|
FTextLayout::FLineModel::FLineModel( const TSharedRef< FString >& InText )
|
|
: Text( InText )
|
|
, ShapedTextCache( FShapedTextCache::Create(*FSlateApplication::Get().GetRenderer()->GetFontCache()) )
|
|
, TextBaseDirection( TextBiDi::ETextDirection::LeftToRight )
|
|
, Runs()
|
|
, BreakCandidates()
|
|
, RunRenderers()
|
|
, DirtyFlags( ELineModelDirtyState::All )
|
|
{
|
|
}
|
|
|
|
void FTextLayout::FRunModel::ClearCache()
|
|
{
|
|
MeasuredRanges.Empty();
|
|
MeasuredRangeSizes.Empty();
|
|
}
|
|
|
|
void FTextLayout::FRunModel::AppendTextTo(FString& Text) const
|
|
{
|
|
Run->AppendTextTo(Text);
|
|
}
|
|
|
|
void FTextLayout::FRunModel::AppendTextTo(FString& Text, const FTextRange& Range) const
|
|
{
|
|
Run->AppendTextTo(Text, Range);
|
|
}
|
|
|
|
TSharedRef< ILayoutBlock > FTextLayout::FRunModel::CreateBlock( const FBlockDefinition& BlockDefine, float InScale, const FLayoutBlockTextContext& InTextContext ) const
|
|
{
|
|
const FTextRange& SizeRange = BlockDefine.ActualRange;
|
|
|
|
if ( MeasuredRanges.Num() == 0 )
|
|
{
|
|
FTextRange RunRange = Run->GetTextRange();
|
|
return Run->CreateBlock( BlockDefine.ActualRange.BeginIndex, BlockDefine.ActualRange.EndIndex, Run->Measure( SizeRange.BeginIndex, SizeRange.EndIndex, InScale, InTextContext ), InTextContext, BlockDefine.Renderer );
|
|
}
|
|
|
|
int32 StartRangeIndex = 0;
|
|
int32 EndRangeIndex = 0;
|
|
|
|
if ( MeasuredRanges.Num() > 16 )
|
|
{
|
|
if ( SizeRange.BeginIndex != 0 )
|
|
{
|
|
StartRangeIndex = BinarySearchForBeginIndex( MeasuredRanges, SizeRange.BeginIndex );
|
|
check( StartRangeIndex != INDEX_NONE);
|
|
}
|
|
|
|
EndRangeIndex = StartRangeIndex;
|
|
if ( StartRangeIndex != MeasuredRanges.Num() - 1 )
|
|
{
|
|
EndRangeIndex = BinarySearchForEndIndex( MeasuredRanges, StartRangeIndex, SizeRange.EndIndex );
|
|
check( EndRangeIndex != INDEX_NONE);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const int32 MaxValidIndex = MeasuredRanges.Num() - 1;
|
|
|
|
if ( SizeRange.BeginIndex != 0 )
|
|
{
|
|
bool FoundBeginIndexMatch = false;
|
|
for (; StartRangeIndex < MaxValidIndex; StartRangeIndex++)
|
|
{
|
|
FoundBeginIndexMatch = MeasuredRanges[ StartRangeIndex ].BeginIndex >= SizeRange.BeginIndex;
|
|
if ( FoundBeginIndexMatch )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
check( FoundBeginIndexMatch == true || StartRangeIndex == MaxValidIndex );
|
|
}
|
|
|
|
EndRangeIndex = StartRangeIndex;
|
|
if ( StartRangeIndex != MaxValidIndex )
|
|
{
|
|
bool FoundEndIndexMatch = false;
|
|
for (; EndRangeIndex < MeasuredRanges.Num(); EndRangeIndex++)
|
|
{
|
|
FoundEndIndexMatch = MeasuredRanges[ EndRangeIndex ].EndIndex >= SizeRange.EndIndex;
|
|
if ( FoundEndIndexMatch )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
check( FoundEndIndexMatch == true || EndRangeIndex == MaxValidIndex );
|
|
}
|
|
}
|
|
|
|
FVector2D BlockSize( ForceInitToZero );
|
|
if ( StartRangeIndex == EndRangeIndex )
|
|
{
|
|
if ( MeasuredRanges[ StartRangeIndex ].BeginIndex == SizeRange.BeginIndex &&
|
|
MeasuredRanges[ StartRangeIndex ].EndIndex == SizeRange.EndIndex )
|
|
{
|
|
BlockSize += MeasuredRangeSizes[ StartRangeIndex ];
|
|
}
|
|
else
|
|
{
|
|
BlockSize += Run->Measure( SizeRange.BeginIndex, SizeRange.EndIndex, InScale, InTextContext );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( MeasuredRanges[ StartRangeIndex ].BeginIndex == SizeRange.BeginIndex )
|
|
{
|
|
BlockSize += MeasuredRangeSizes[ StartRangeIndex ];
|
|
}
|
|
else
|
|
{
|
|
BlockSize += Run->Measure( SizeRange.BeginIndex, MeasuredRanges[ StartRangeIndex ].EndIndex, InScale, InTextContext );
|
|
}
|
|
|
|
for (int32 Index = StartRangeIndex + 1; Index < EndRangeIndex; Index++)
|
|
{
|
|
BlockSize.X += MeasuredRangeSizes[ Index ].X;
|
|
BlockSize.Y = FMath::Max( MeasuredRangeSizes[ Index ].Y, BlockSize.Y );
|
|
}
|
|
|
|
if ( MeasuredRanges[ EndRangeIndex ].EndIndex == SizeRange.EndIndex )
|
|
{
|
|
BlockSize.X += MeasuredRangeSizes[ EndRangeIndex ].X;
|
|
BlockSize.Y = FMath::Max( MeasuredRangeSizes[ EndRangeIndex ].Y, BlockSize.Y );
|
|
}
|
|
else
|
|
{
|
|
FVector2D Size = Run->Measure( MeasuredRanges[ EndRangeIndex ].BeginIndex, SizeRange.EndIndex, InScale, InTextContext );
|
|
BlockSize.X += Size.X;
|
|
BlockSize.Y = FMath::Max( Size.Y, BlockSize.Y );
|
|
}
|
|
}
|
|
|
|
return Run->CreateBlock( BlockDefine.ActualRange.BeginIndex, BlockDefine.ActualRange.EndIndex, BlockSize, InTextContext, BlockDefine.Renderer );
|
|
}
|
|
|
|
int32 FTextLayout::FRunModel::BinarySearchForEndIndex( const TArray< FTextRange >& Ranges, int32 RangeBeginIndex, int32 EndIndex )
|
|
{
|
|
int32 Min = RangeBeginIndex;
|
|
int32 Mid = 0;
|
|
int32 Max = Ranges.Num() - 1;
|
|
while (Max >= Min)
|
|
{
|
|
Mid = Min + ((Max - Min) / 2);
|
|
if ( Ranges[Mid].EndIndex == EndIndex )
|
|
{
|
|
return Mid;
|
|
}
|
|
else if ( Ranges[Mid].EndIndex < EndIndex )
|
|
{
|
|
Min = Mid + 1;
|
|
}
|
|
else
|
|
{
|
|
Max = Mid - 1;
|
|
}
|
|
}
|
|
|
|
return Mid;
|
|
}
|
|
|
|
int32 FTextLayout::FRunModel::BinarySearchForBeginIndex( const TArray< FTextRange >& Ranges, int32 BeginIndex )
|
|
{
|
|
int32 Min = 0;
|
|
int32 Mid = 0;
|
|
int32 Max = Ranges.Num() - 1;
|
|
while (Max >= Min)
|
|
{
|
|
Mid = Min + ((Max - Min) / 2);
|
|
if ( Ranges[Mid].BeginIndex == BeginIndex )
|
|
{
|
|
return Mid;
|
|
}
|
|
else if ( Ranges[Mid].BeginIndex < BeginIndex )
|
|
{
|
|
Min = Mid + 1;
|
|
}
|
|
else
|
|
{
|
|
Max = Mid - 1;
|
|
}
|
|
}
|
|
|
|
return Mid;
|
|
}
|
|
|
|
uint8 FTextLayout::FRunModel::GetKerning(int32 CurrentIndex, float InScale, const FRunTextContext& InTextContext)
|
|
{
|
|
return Run->GetKerning(CurrentIndex, InScale, InTextContext);
|
|
}
|
|
|
|
FVector2D FTextLayout::FRunModel::Measure(int32 BeginIndex, int32 EndIndex, float InScale, const FRunTextContext& InTextContext)
|
|
{
|
|
FVector2D Size = Run->Measure(BeginIndex, EndIndex, InScale, InTextContext);
|
|
|
|
MeasuredRanges.Add( FTextRange( BeginIndex, EndIndex ) );
|
|
MeasuredRangeSizes.Add( Size );
|
|
|
|
return Size;
|
|
}
|
|
|
|
int16 FTextLayout::FRunModel::GetMaxHeight(float InScale) const
|
|
{
|
|
return Run->GetMaxHeight(InScale);
|
|
}
|
|
|
|
int16 FTextLayout::FRunModel::GetBaseLine( float InScale ) const
|
|
{
|
|
return Run->GetBaseLine(InScale);
|
|
}
|
|
|
|
FTextRange FTextLayout::FRunModel::GetTextRange() const
|
|
{
|
|
return Run->GetTextRange();
|
|
}
|
|
|
|
void FTextLayout::FRunModel::SetTextRange( const FTextRange& Value )
|
|
{
|
|
Run->SetTextRange( Value );
|
|
}
|
|
|
|
void FTextLayout::FRunModel::EndLayout()
|
|
{
|
|
Run->EndLayout();
|
|
}
|
|
|
|
void FTextLayout::FRunModel::BeginLayout()
|
|
{
|
|
Run->BeginLayout();
|
|
}
|
|
|
|
TSharedRef< IRun > FTextLayout::FRunModel::GetRun() const
|
|
{
|
|
return Run;
|
|
}
|
|
|
|
FTextLayout::FRunModel::FRunModel( const TSharedRef< IRun >& InRun ) : Run( InRun )
|
|
{
|
|
|
|
}
|
|
|
|
int32 FTextLayout::FTextOffsetLocations::TextLocationToOffset(const FTextLocation& InLocation) const
|
|
{
|
|
const int32 LineIndex = InLocation.GetLineIndex();
|
|
if(LineIndex >= 0 && LineIndex < OffsetData.Num())
|
|
{
|
|
const FOffsetEntry& OffsetEntry = OffsetData[LineIndex];
|
|
return OffsetEntry.FlatStringIndex + InLocation.GetOffset();
|
|
}
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
FTextLocation FTextLayout::FTextOffsetLocations::OffsetToTextLocation(const int32 InOffset) const
|
|
{
|
|
for(int32 OffsetEntryIndex = 0; OffsetEntryIndex < OffsetData.Num(); ++OffsetEntryIndex)
|
|
{
|
|
const FOffsetEntry& OffsetEntry = OffsetData[OffsetEntryIndex];
|
|
const FTextRange FlatLineRange(OffsetEntry.FlatStringIndex, OffsetEntry.FlatStringIndex + OffsetEntry.DocumentLineLength);
|
|
if(FlatLineRange.InclusiveContains(InOffset))
|
|
{
|
|
const int32 OffsetWithinLine = InOffset - OffsetEntry.FlatStringIndex;
|
|
return FTextLocation(OffsetEntryIndex, OffsetWithinLine);
|
|
}
|
|
}
|
|
return FTextLocation();
|
|
}
|
|
|
|
int32 FTextLayout::FTextOffsetLocations::GetTextLength() const
|
|
{
|
|
if(OffsetData.Num())
|
|
{
|
|
const FOffsetEntry& OffsetEntry = OffsetData.Last();
|
|
return OffsetEntry.FlatStringIndex + OffsetEntry.DocumentLineLength;
|
|
}
|
|
return 0;
|
|
} |