From bfa795dc02eedf8fc70a5502700e8281481249c7 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sat, 8 Jul 2023 04:56:22 +0100 Subject: [PATCH 01/12] - Remove external export macro to better share fixes. --- DialogueBox.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DialogueBox.h b/DialogueBox.h index 7597a1a..0b9b7cc 100644 --- a/DialogueBox.h +++ b/DialogueBox.h @@ -43,7 +43,7 @@ struct FDialogueTextSegment }; UCLASS() -class FLAME_API UDialogueBox : public UUserWidget +class UDialogueBox : public UUserWidget { GENERATED_BODY() From 8acbe6571deab06a68db294c78581fc9e0c2f3be Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Fri, 14 Jul 2023 17:39:55 +0100 Subject: [PATCH 02/12] - Reworked implementation to use text runs rather than pre-calculating the layout. Still tidying and testing this implementation. --- DialogueBox.cpp | 304 ++++++++++++++++++++++++++++++++---------------- DialogueBox.h | 22 ++-- 2 files changed, 215 insertions(+), 111 deletions(-) diff --git a/DialogueBox.cpp b/DialogueBox.cpp index 493fde9..fdf363c 100644 --- a/DialogueBox.cpp +++ b/DialogueBox.cpp @@ -6,6 +6,145 @@ #include "Widgets/Text/SRichTextBlock.h" #include "TimerManager.h" +#include +#include +#include + +class FDialogueBoxTextRun : public FSlateTextRun +{ +public: + FVector2D MeasureInternal( int32 BeginIndex, int32 EndIndex, float Scale, const FRunTextContext& TextContext, const FString& InText ) const + { + const FVector2D ShadowOffsetToApply((EndIndex == Range.EndIndex) ? FMath::Abs(Style.ShadowOffset.X * Scale) : 0.0f, FMath::Abs(Style.ShadowOffset.Y * Scale)); + + // 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. + const float ScaledOutlineSize = Style.Font.OutlineSettings.OutlineSize * Scale; + const FVector2D OutlineSizeToApply((BeginIndex == Range.BeginIndex ? ScaledOutlineSize : 0) + (EndIndex == Range.EndIndex ? ScaledOutlineSize : 0), ScaledOutlineSize); + + if (EndIndex - BeginIndex == 0) + { + return FVector2D(0, GetMaxHeight(Scale)) + ShadowOffsetToApply + OutlineSizeToApply; + } + + // 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, InText.Len()), Scale, TextContext, Style.Font), FTextRange(BeginIndex, EndIndex), *InText) + ShadowOffsetToApply + OutlineSizeToApply; + } + + FVector2D Measure(int32 StartIndex, int32 EndIndex, float Scale, const FRunTextContext& TextContext) const override + { + if (EndIndex != Range.EndIndex) + { + return FSlateTextRun::Measure(StartIndex, EndIndex, Scale, TextContext); + } + + int32 partialContent = Range.Len(); + check(partialContent >= 0); + + FString futureContent; + if (!Segment.RunInfo.ContentRange.IsEmpty()) + { + // with tags + futureContent = Segment.Text.Mid(Segment.RunInfo.ContentRange.BeginIndex - Segment.RunInfo.OriginalRange.BeginIndex + partialContent, Segment.RunInfo.ContentRange.Len() - partialContent); + } + else + { + // no tags + futureContent = Segment.Text.Mid(partialContent, Segment.RunInfo.OriginalRange.Len() - partialContent); + } + for (int32 i = 0; i < futureContent.Len(); ++i) + { + TCHAR futureChar = futureContent[i]; + if (FText::IsWhitespace(futureChar)) + { + futureContent.LeftInline(i); + break; + } + } + + FString combinedContent = *Text + futureContent; + return MeasureInternal(StartIndex, combinedContent.Len(), Scale, TextContext, combinedContent); + } + + FDialogueBoxTextRun(const FRunInfo& InRunInfo, const TSharedRef< const FString >& InText, const FTextBlockStyle& InStyle, const FDialogueTextSegment& Segment) + : + FSlateTextRun(InRunInfo, InText, InStyle), + Segment(Segment) + { + } + + FDialogueBoxTextRun(const FRunInfo& InRunInfo, const TSharedRef< const FString >& InText, const FTextBlockStyle& InStyle, const FTextRange& InRange, const FDialogueTextSegment& Segment) + : + FSlateTextRun(InRunInfo, InText, InStyle, InRange), + Segment(Segment) + { + } + +private: + const FDialogueTextSegment& Segment; +}; + +class FDialogueBoxTextDecorator : public ITextDecorator +{ +public: + FDialogueBoxTextDecorator(const TArray* Segments, const int32* CurrentSegmentIndex) + : + Segments(Segments), + CurrentSegmentIndex(CurrentSegmentIndex) + { + } + + bool Supports(const FTextRunParseResults& RunInfo, const FString& Text) const override + { + // no segments have been calculated yet + if (*CurrentSegmentIndex >= Segments->Num()) + { + return false; + } + + // does this run relate to the segment which is still in-flight? + const FDialogueTextSegment& segment = (*Segments)[*CurrentSegmentIndex]; + const FTextRange& segmentRange = !RunInfo.ContentRange.IsEmpty() ? segment.RunInfo.ContentRange : segment.RunInfo.OriginalRange; + const FTextRange& runRange = !RunInfo.ContentRange.IsEmpty() ? RunInfo.ContentRange : RunInfo.OriginalRange; + auto intersected = runRange.Intersect(segmentRange); + return !intersected.IsEmpty() && segmentRange != intersected; + } + + TSharedRef Create(const TSharedRef& TextLayout, const FTextRunParseResults& InRunInfo, const FString& ProcessedString, const TSharedRef& InOutModelText, const ISlateStyle* InStyle) override + { + FRunInfo RunInfo(InRunInfo.Name); + for (const TPair& Pair : InRunInfo.MetaData) + { + int32 Length = FMath::Max(0, Pair.Value.EndIndex - Pair.Value.BeginIndex); + RunInfo.MetaData.Add(Pair.Key, ProcessedString.Mid(Pair.Value.BeginIndex, Length)); + } + + const FTextBlockStyle* TextBlockStyle; + FTextRange ModelRange; + ModelRange.BeginIndex = InOutModelText->Len(); + if (!(InRunInfo.Name.IsEmpty()) && InStyle->HasWidgetStyle< FTextBlockStyle >(FName(*InRunInfo.Name))) + { + *InOutModelText += ProcessedString.Mid(InRunInfo.ContentRange.BeginIndex, InRunInfo.ContentRange.Len()); + TextBlockStyle = &(InStyle->GetWidgetStyle< FTextBlockStyle >(FName(*InRunInfo.Name))); + } + else + { + *InOutModelText += ProcessedString.Mid(InRunInfo.OriginalRange.BeginIndex, InRunInfo.OriginalRange.Len()); + TextBlockStyle = &static_cast(*TextLayout).GetDefaultTextStyle(); + } + ModelRange.EndIndex = InOutModelText->Len(); + + const FDialogueTextSegment& Segment = (*Segments)[*CurrentSegmentIndex]; + return MakeShared(RunInfo, InOutModelText, *TextBlockStyle, ModelRange, Segment); + } + +private: + const TArray* Segments; + const int32* CurrentSegmentIndex; +}; + + + TSharedRef UDialogueTextBlock::RebuildWidget() { // Copied from URichTextBlock::RebuildWidget @@ -14,18 +153,17 @@ TSharedRef UDialogueTextBlock::RebuildWidget() TArray< TSharedRef< class ITextDecorator > > CreatedDecorators; CreateDecorators(CreatedDecorators); - TextMarshaller = FRichTextLayoutMarshaller::Create(CreateMarkupParser(), CreateMarkupWriter(), CreatedDecorators, StyleInstance.Get()); + TextParser = CreateMarkupParser(); + TSharedRef Marshaller = FRichTextLayoutMarshaller::Create(TextParser, CreateMarkupWriter(), CreatedDecorators, StyleInstance.Get()); + if (Segments && CurrentSegmentIndex) + { + Marshaller->AppendInlineDecorator(MakeShared(Segments, CurrentSegmentIndex)); + } MyRichTextBlock = SNew(SRichTextBlock) .TextStyle(bOverrideDefaultStyle ? &DefaultTextStyleOverride : &DefaultTextStyle) - .Marshaller(TextMarshaller) - .CreateSlateTextLayout( - FCreateSlateTextLayout::CreateWeakLambda(this, [this] (SWidget* InOwner, const FTextBlockStyle& InDefaultTextStyle) mutable - { - TextLayout = FSlateTextLayout::Create(InOwner, InDefaultTextStyle); - return StaticCastSharedPtr(TextLayout).ToSharedRef(); - })); + .Marshaller(Marshaller); return MyRichTextBlock.ToSharedRef(); } @@ -45,7 +183,6 @@ void UDialogueBox::PlayLine(const FText& InLine) CurrentLine = InLine; CurrentLetterIndex = 0; - CachedLetterIndex = 0; CurrentSegmentIndex = 0; MaxLetterIndex = 0; Segments.Empty(); @@ -96,6 +233,13 @@ void UDialogueBox::SkipToLineEnd() OnLineFinishedPlaying(); } +void UDialogueBox::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + + LineText->ConfigureFromParent(&Segments, &CurrentSegmentIndex); +} + void UDialogueBox::PlayNextLetter() { if (Segments.IsEmpty()) @@ -133,129 +277,83 @@ void UDialogueBox::PlayNextLetter() } } -// TODO: Need to recalculate this + CalculateSegments when the text box gets resized. void UDialogueBox::CalculateWrappedString() { - if (IsValid(LineText) && LineText->GetTextLayout().IsValid()) + if (IsValid(LineText) && LineText->GetTextParser().IsValid()) { - TSharedPtr Layout = LineText->GetTextLayout(); - TSharedPtr Marshaller = LineText->GetTextMarshaller(); - - const FGeometry& TextBoxGeometry = LineText->GetCachedGeometry(); - const FVector2D TextBoxSize = TextBoxGeometry.GetLocalSize(); + TSharedPtr Parser = LineText->GetTextParser(); - Layout->SetWrappingWidth(TextBoxSize.X); - Marshaller->SetText(CurrentLine.ToString(), *Layout.Get()); - Layout->UpdateIfNeeded(); - - bool bHasWrittenText = false; - for (const FTextLayout::FLineView& View: Layout->GetLineViews()) + TArray Lines; + FString ProcessedString; + Parser->Process(Lines, CurrentLine.ToString(), ProcessedString); + for (int32 LineIdx = 0; LineIdx < Lines.Num(); ++LineIdx) { - const FTextLayout::FLineModel& Model = Layout->GetLineModels()[View.ModelIndex]; - - for (TSharedRef Block : View.Blocks) + const FTextLineParseResults& Line = Lines[LineIdx]; + for (const FTextRunParseResults& Run : Line.Runs) { - TSharedRef Run = Block->GetRun(); - - FDialogueTextSegment Segment; - Run->AppendTextTo(Segment.Text, Block->GetTextRange()); - - // HACK: For some reason image decorators (and possibly other decorators that don't - // have actual text inside them) result in the run containing a zero width space instead of - // nothing. This messes up our checks for whether the text is empty or not, which doesn't - // have an effect on image decorators but might cause issues for other custom ones. - if (Segment.Text.Len() == 1 && Segment.Text[0] == 0x200B) - { - Segment.Text.Empty(); - } - - Segment.RunInfo = Run->GetRunInfo(); - Segments.Add(Segment); - - // A segment with a named run should still take up time for the typewriter effect. - MaxLetterIndex += FMath::Max(Segment.Text.Len(), Segment.RunInfo.Name.IsEmpty() ? 0 : 1); - - if (!Segment.Text.IsEmpty() || !Segment.RunInfo.Name.IsEmpty()) - { - bHasWrittenText = true; - } + Segments.Emplace( + FDialogueTextSegment + { + ProcessedString.Mid(Run.OriginalRange.BeginIndex, Run.OriginalRange.Len()), + Run + }); } - if (bHasWrittenText) + if (LineIdx != Lines.Num() - 1) { - Segments.Add(FDialogueTextSegment{TEXT("\n")}); + Segments.Emplace( + FDialogueTextSegment + { + TEXT("\n"), + FTextRunParseResults(FString(), FTextRange(0, 1)) + }); ++MaxLetterIndex; } - } - Layout->SetWrappingWidth(0); - LineText->SetText(LineText->GetText()); - } - else - { - Segments.Add(FDialogueTextSegment{CurrentLine.ToString()}); - MaxLetterIndex = Segments[0].Text.Len(); + MaxLetterIndex = Line.Range.EndIndex; + } } } FString UDialogueBox::CalculateSegments() { - FString Result = CachedSegmentText; - - int32 Idx = CachedLetterIndex; - while (Idx <= CurrentLetterIndex && CurrentSegmentIndex < Segments.Num()) + while (CurrentSegmentIndex < Segments.Num()) { const FDialogueTextSegment& Segment = Segments[CurrentSegmentIndex]; - if (!Segment.RunInfo.Name.IsEmpty()) - { - Result += FString::Printf(TEXT("<%s"), *Segment.RunInfo.Name); - if (!Segment.RunInfo.MetaData.IsEmpty()) - { - for (const TTuple& MetaData : Segment.RunInfo.MetaData) - { - Result += FString::Printf(TEXT(" %s=\"%s\""), *MetaData.Key, *MetaData.Value); - } - } + int32 SegmentStartIndex = std::max(Segment.RunInfo.OriginalRange.BeginIndex, Segment.RunInfo.ContentRange.BeginIndex); + CurrentLetterIndex = std::max(CurrentLetterIndex, SegmentStartIndex); - if (Segment.Text.IsEmpty()) - { - Result += TEXT("/>"); - ++Idx; // This still takes up an index for the typewriter effect. - } - else - { - Result += TEXT(">"); - } + if (Segment.RunInfo.ContentRange.IsEmpty() ? !Segment.RunInfo.OriginalRange.Contains(CurrentLetterIndex) : !Segment.RunInfo.ContentRange.Contains(CurrentLetterIndex)) + { + CachedSegmentText += Segment.Text; + CurrentSegmentIndex++; + continue; } - bool bIsSegmentComplete = true; - if (!Segment.Text.IsEmpty()) + // is this segment an inline tag? + if (!Segment.RunInfo.Name.IsEmpty() && !Segment.RunInfo.MetaData.IsEmpty()) { - int32 LettersLeft = CurrentLetterIndex - Idx + 1; - bIsSegmentComplete = LettersLeft >= Segment.Text.Len(); - LettersLeft = FMath::Min(LettersLeft, Segment.Text.Len()); - Idx += LettersLeft; - - Result += Segment.Text.Mid(0, LettersLeft); + // seek to end of tag - treat as single character + int32 SegmentEndIndex = std::max(Segment.RunInfo.OriginalRange.EndIndex, Segment.RunInfo.ContentRange.EndIndex); + CurrentLetterIndex = std::max(CurrentLetterIndex, SegmentEndIndex); + return CachedSegmentText + Segment.Text; + } + // is this segment partially typed? + else if (Segment.RunInfo.OriginalRange.Contains(CurrentLetterIndex)) + { + FString Result = CachedSegmentText + Segment.Text.Mid(0, CurrentLetterIndex - Segment.RunInfo.OriginalRange.BeginIndex); - if (!Segment.RunInfo.Name.IsEmpty()) + // if content tags need closing, append the remaining tag characters + if (!Segment.RunInfo.ContentRange.IsEmpty() && Segment.RunInfo.ContentRange.Contains(CurrentLetterIndex)) { - Result += TEXT(""); + Result += Segment.Text.Mid(Segment.RunInfo.ContentRange.EndIndex - Segment.RunInfo.OriginalRange.BeginIndex, Segment.RunInfo.OriginalRange.EndIndex - Segment.RunInfo.ContentRange.EndIndex); } - } - if (bIsSegmentComplete) - { - CachedLetterIndex = Idx; - CachedSegmentText = Result; - ++CurrentSegmentIndex; - } - else - { - break; + return Result; } + break; } - return Result; + return CachedSegmentText; } \ No newline at end of file diff --git a/DialogueBox.h b/DialogueBox.h index 0b9b7cc..c69f297 100644 --- a/DialogueBox.h +++ b/DialogueBox.h @@ -9,6 +9,8 @@ #include "Framework/Text/SlateTextLayout.h" #include "DialogueBox.generated.h" +struct FDialogueTextSegment; + /** * A text block that exposes more information about text layout. */ @@ -18,28 +20,31 @@ class UDialogueTextBlock : public URichTextBlock GENERATED_BODY() public: - FORCEINLINE TSharedPtr GetTextLayout() const + FORCEINLINE TSharedPtr GetTextParser() const { - return TextLayout; + return TextParser; } - FORCEINLINE TSharedPtr GetTextMarshaller() const + FORCEINLINE void ConfigureFromParent(const TArray* InSegments, const int32* InCurrentSegmentIndex) { - return TextMarshaller; + Segments = InSegments; + CurrentSegmentIndex = InCurrentSegmentIndex; } protected: virtual TSharedRef RebuildWidget() override; private: - TSharedPtr TextLayout; - TSharedPtr TextMarshaller; + TSharedPtr TextParser; + + const TArray* Segments; + const int32* CurrentSegmentIndex; }; struct FDialogueTextSegment { FString Text; - FRunInfo RunInfo; + FTextRunParseResults RunInfo; }; UCLASS() @@ -81,6 +86,8 @@ class UDialogueBox : public UUserWidget UFUNCTION(BlueprintImplementableEvent, Category = "Dialogue Box") void OnLineFinishedPlaying(); + void NativeOnInitialized() override; + private: void PlayNextLetter(); @@ -97,7 +104,6 @@ class UDialogueBox : public UUserWidget // everything as the last few characters of a string may change if they're related to // a named run that hasn't been completed yet. FString CachedSegmentText; - int32 CachedLetterIndex = 0; int32 CurrentSegmentIndex = 0; int32 CurrentLetterIndex = 0; From c23f844d9a333854121e49f13b1de99f939e8021 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sat, 15 Jul 2023 04:56:29 +0100 Subject: [PATCH 03/12] - Code quality and misc fixes/testing. --- DialogueBox.cpp | 112 +++++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/DialogueBox.cpp b/DialogueBox.cpp index fdf363c..3c46dc2 100644 --- a/DialogueBox.cpp +++ b/DialogueBox.cpp @@ -10,48 +10,53 @@ #include #include -class FDialogueBoxTextRun : public FSlateTextRun +/** + * Text run that represents a segment of text which is in the process of being typed out. + * The size of the text block will represent the final size of each word rather than the provided content. + */ +class FPartialDialogueRun : public FSlateTextRun { public: - FVector2D MeasureInternal( int32 BeginIndex, int32 EndIndex, float Scale, const FRunTextContext& TextContext, const FString& InText ) const + FPartialDialogueRun(const FRunInfo& InRunInfo, const TSharedRef< const FString >& InText, const FTextBlockStyle& InStyle, const FTextRange& InRange, const FDialogueTextSegment& Segment) + : + FSlateTextRun(InRunInfo, InText, InStyle, InRange), + Segment(Segment) { - const FVector2D ShadowOffsetToApply((EndIndex == Range.EndIndex) ? FMath::Abs(Style.ShadowOffset.X * Scale) : 0.0f, FMath::Abs(Style.ShadowOffset.Y * Scale)); - - // 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. - const float ScaledOutlineSize = Style.Font.OutlineSettings.OutlineSize * Scale; - const FVector2D OutlineSizeToApply((BeginIndex == Range.BeginIndex ? ScaledOutlineSize : 0) + (EndIndex == Range.EndIndex ? ScaledOutlineSize : 0), ScaledOutlineSize); - - if (EndIndex - BeginIndex == 0) - { - return FVector2D(0, GetMaxHeight(Scale)) + ShadowOffsetToApply + OutlineSizeToApply; - } - - // 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, InText.Len()), Scale, TextContext, Style.Font), FTextRange(BeginIndex, EndIndex), *InText) + ShadowOffsetToApply + OutlineSizeToApply; } FVector2D Measure(int32 StartIndex, int32 EndIndex, float Scale, const FRunTextContext& TextContext) const override { if (EndIndex != Range.EndIndex) { + // measuring text within existing range, refer to normal implementation return FSlateTextRun::Measure(StartIndex, EndIndex, Scale, TextContext); } + else + { + // attempting to measure to end of typed range, construct future typed content from source segment and measure based on that instead. + // this will ensure text is wrapped prior to being fully typed. + FString combinedContent = ConstructCombinedText(); + return MeasureInternal(StartIndex, combinedContent.Len(), Scale, TextContext, combinedContent); + } + } - int32 partialContent = Range.Len(); - check(partialContent >= 0); +private: + FString ConstructCombinedText() const + { + const int32 existingChars = Range.Len(); FString futureContent; if (!Segment.RunInfo.ContentRange.IsEmpty()) { // with tags - futureContent = Segment.Text.Mid(Segment.RunInfo.ContentRange.BeginIndex - Segment.RunInfo.OriginalRange.BeginIndex + partialContent, Segment.RunInfo.ContentRange.Len() - partialContent); + futureContent = Segment.Text.Mid(Segment.RunInfo.ContentRange.BeginIndex - Segment.RunInfo.OriginalRange.BeginIndex + existingChars, Segment.RunInfo.ContentRange.Len() - existingChars); } else { // no tags - futureContent = Segment.Text.Mid(partialContent, Segment.RunInfo.OriginalRange.Len() - partialContent); + futureContent = Segment.Text.Mid(existingChars, Segment.RunInfo.OriginalRange.Len() - existingChars); } + // trim to next possible wrap opportunity for (int32 i = 0; i < futureContent.Len(); ++i) { TCHAR futureChar = futureContent[i]; @@ -62,32 +67,37 @@ class FDialogueBoxTextRun : public FSlateTextRun } } - FString combinedContent = *Text + futureContent; - return MeasureInternal(StartIndex, combinedContent.Len(), Scale, TextContext, combinedContent); + return *Text + futureContent; } - FDialogueBoxTextRun(const FRunInfo& InRunInfo, const TSharedRef< const FString >& InText, const FTextBlockStyle& InStyle, const FDialogueTextSegment& Segment) - : - FSlateTextRun(InRunInfo, InText, InStyle), - Segment(Segment) + FVector2D MeasureInternal(int32 BeginIndex, int32 EndIndex, float Scale, const FRunTextContext& TextContext, const FString& InText) const { - } + const FVector2D ShadowOffsetToApply((EndIndex == Range.EndIndex) ? FMath::Abs(Style.ShadowOffset.X * Scale) : 0.0f, FMath::Abs(Style.ShadowOffset.Y * Scale)); - FDialogueBoxTextRun(const FRunInfo& InRunInfo, const TSharedRef< const FString >& InText, const FTextBlockStyle& InStyle, const FTextRange& InRange, const FDialogueTextSegment& Segment) - : - FSlateTextRun(InRunInfo, InText, InStyle, InRange), - Segment(Segment) - { + // 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. + const float ScaledOutlineSize = Style.Font.OutlineSettings.OutlineSize * Scale; + const FVector2D OutlineSizeToApply((BeginIndex == Range.BeginIndex ? ScaledOutlineSize : 0) + (EndIndex == Range.EndIndex ? ScaledOutlineSize : 0), ScaledOutlineSize); + + if (EndIndex - BeginIndex == 0) + { + return FVector2D(0, GetMaxHeight(Scale)) + ShadowOffsetToApply + OutlineSizeToApply; + } + + // 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, InText.Len()), Scale, TextContext, Style.Font), FTextRange(BeginIndex, EndIndex), *InText) + ShadowOffsetToApply + OutlineSizeToApply; } -private: const FDialogueTextSegment& Segment; }; -class FDialogueBoxTextDecorator : public ITextDecorator +/** + * A decorator that intercepts partially typed segments and allocates an FPartialDialogueRun to represent them. + */ +class FPartialDialogueDecorator : public ITextDecorator { public: - FDialogueBoxTextDecorator(const TArray* Segments, const int32* CurrentSegmentIndex) + FPartialDialogueDecorator(const TArray* Segments, const int32* CurrentSegmentIndex) : Segments(Segments), CurrentSegmentIndex(CurrentSegmentIndex) @@ -112,6 +122,7 @@ class FDialogueBoxTextDecorator : public ITextDecorator TSharedRef Create(const TSharedRef& TextLayout, const FTextRunParseResults& InRunInfo, const FString& ProcessedString, const TSharedRef& InOutModelText, const ISlateStyle* InStyle) override { + // copied from FRichTextLayoutMarshaller::AppendRunsForText FRunInfo RunInfo(InRunInfo.Name); for (const TPair& Pair : InRunInfo.MetaData) { @@ -119,23 +130,18 @@ class FDialogueBoxTextDecorator : public ITextDecorator RunInfo.MetaData.Add(Pair.Key, ProcessedString.Mid(Pair.Value.BeginIndex, Length)); } - const FTextBlockStyle* TextBlockStyle; - FTextRange ModelRange; - ModelRange.BeginIndex = InOutModelText->Len(); - if (!(InRunInfo.Name.IsEmpty()) && InStyle->HasWidgetStyle< FTextBlockStyle >(FName(*InRunInfo.Name))) - { - *InOutModelText += ProcessedString.Mid(InRunInfo.ContentRange.BeginIndex, InRunInfo.ContentRange.Len()); - TextBlockStyle = &(InStyle->GetWidgetStyle< FTextBlockStyle >(FName(*InRunInfo.Name))); - } - else - { - *InOutModelText += ProcessedString.Mid(InRunInfo.OriginalRange.BeginIndex, InRunInfo.OriginalRange.Len()); - TextBlockStyle = &static_cast(*TextLayout).GetDefaultTextStyle(); - } - ModelRange.EndIndex = InOutModelText->Len(); + // resolve text style + const bool CanParseTags = !InRunInfo.Name.IsEmpty() && InStyle->HasWidgetStyle< FTextBlockStyle >(*InRunInfo.Name); + const FTextBlockStyle& Style = CanParseTags ? InStyle->GetWidgetStyle< FTextBlockStyle >(*InRunInfo.Name) : static_cast(*TextLayout).GetDefaultTextStyle(); + + // skip tags if valid style parser found + const FTextRange& Range = CanParseTags ? InRunInfo.ContentRange : InRunInfo.OriginalRange; + FTextRange ModelRange(InOutModelText->Len(), InOutModelText->Len() + Range.Len()); const FDialogueTextSegment& Segment = (*Segments)[*CurrentSegmentIndex]; - return MakeShared(RunInfo, InOutModelText, *TextBlockStyle, ModelRange, Segment); + *InOutModelText += Segment.Text.Mid(Range.BeginIndex - Segment.RunInfo.OriginalRange.BeginIndex, Range.Len()); + + return MakeShared(RunInfo, InOutModelText, Style, ModelRange, Segment); } private: @@ -157,7 +163,7 @@ TSharedRef UDialogueTextBlock::RebuildWidget() TSharedRef Marshaller = FRichTextLayoutMarshaller::Create(TextParser, CreateMarkupWriter(), CreatedDecorators, StyleInstance.Get()); if (Segments && CurrentSegmentIndex) { - Marshaller->AppendInlineDecorator(MakeShared(Segments, CurrentSegmentIndex)); + Marshaller->AppendInlineDecorator(MakeShared(Segments, CurrentSegmentIndex)); } MyRichTextBlock = @@ -331,8 +337,8 @@ FString UDialogueBox::CalculateSegments() continue; } - // is this segment an inline tag? - if (!Segment.RunInfo.Name.IsEmpty() && !Segment.RunInfo.MetaData.IsEmpty()) + // is this segment an inline tag? eg. + if (!Segment.RunInfo.Name.IsEmpty() && !Segment.RunInfo.OriginalRange.IsEmpty() && Segment.RunInfo.ContentRange.IsEmpty()) { // seek to end of tag - treat as single character int32 SegmentEndIndex = std::max(Segment.RunInfo.OriginalRange.EndIndex, Segment.RunInfo.ContentRange.EndIndex); From 6d997f27a71dc31d0716105c3aa1cd6997066123 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sat, 15 Jul 2023 05:22:41 +0100 Subject: [PATCH 04/12] - Minor whitespace/comments. --- DialogueBox.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DialogueBox.cpp b/DialogueBox.cpp index 3c46dc2..7a62f21 100644 --- a/DialogueBox.cpp +++ b/DialogueBox.cpp @@ -149,8 +149,6 @@ class FPartialDialogueDecorator : public ITextDecorator const int32* CurrentSegmentIndex; }; - - TSharedRef UDialogueTextBlock::RebuildWidget() { // Copied from URichTextBlock::RebuildWidget @@ -163,6 +161,7 @@ TSharedRef UDialogueTextBlock::RebuildWidget() TSharedRef Marshaller = FRichTextLayoutMarshaller::Create(TextParser, CreateMarkupWriter(), CreatedDecorators, StyleInstance.Get()); if (Segments && CurrentSegmentIndex) { + // add custom decorator to intercept partially typed segments Marshaller->AppendInlineDecorator(MakeShared(Segments, CurrentSegmentIndex)); } From 8b4a711a3bbe1609b7b6a628a8fd198c1d0ecbe1 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sat, 15 Jul 2023 05:41:12 +0100 Subject: [PATCH 05/12] - Minor readme tweak, remove line about layout not being recalculated which is fixed in this fork. --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 94b45e0..e2127b1 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,8 @@ you want. * Changing font-size (or using a decorator that's taller than the existing text) anywhere except at the beginning of a line will result in the entire line "jumping" down slightly to accomodate the new text size. I don't have a solution to this yet and don't see myself using different font sizes much, so it isn't something I'm likely to get to any time soon. -* Text wrapping is only calculated a single time when the first character is played. If the widget is resized for any reason, text will - not respect the new boundaries. This should be simple to solve, I just haven't done it yet. * There may be some hidden i18n issues due to all the conversions between `FString`/`FText` and string indexing. -* This has been tested with UE5.0EA, though it should work fine with earlier/later versions. +* This has been tested with UE 5.2.0, though it should work fine with earlier/later versions. * The current implementation was quickly thrown together (see: hacky) and somewhat unoptimized. Some data is duplicated more than it needs to be, and "segment" calculation is a bit more complex than I'd like. From c96680747a101842ab76ea3a7006488fff1ba92b Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sat, 22 Jul 2023 08:43:50 +0100 Subject: [PATCH 06/12] - Fixed deprecation warning. --- DialogueBox.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DialogueBox.cpp b/DialogueBox.cpp index 7a62f21..d444bd4 100644 --- a/DialogueBox.cpp +++ b/DialogueBox.cpp @@ -167,7 +167,7 @@ TSharedRef UDialogueTextBlock::RebuildWidget() MyRichTextBlock = SNew(SRichTextBlock) - .TextStyle(bOverrideDefaultStyle ? &DefaultTextStyleOverride : &DefaultTextStyle) + .TextStyle(bOverrideDefaultStyle ? &GetDefaultTextStyleOverride() : &DefaultTextStyle) .Marshaller(Marshaller); return MyRichTextBlock.ToSharedRef(); From 85c0442a1adc165d4c70e035d2ea0fae6484370b Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sat, 22 Jul 2023 10:20:37 +0100 Subject: [PATCH 07/12] - Dialogue Boxes now calculate their desired size in terms of the final string contents rather than the partially-typed contents. Allows for more variable use of these widgets in layouts. --- DialogueBox.cpp | 31 ++++++++++++++++++++++++------- DialogueBox.h | 6 ++++++ SDialogueTextBlock.cpp | 32 ++++++++++++++++++++++++++++++++ SDialogueTextBlock.h | 17 +++++++++++++++++ 4 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 SDialogueTextBlock.cpp create mode 100644 SDialogueTextBlock.h diff --git a/DialogueBox.cpp b/DialogueBox.cpp index 7a62f21..58913a9 100644 --- a/DialogueBox.cpp +++ b/DialogueBox.cpp @@ -3,7 +3,7 @@ #include "DialogueBox.h" #include "Engine/Font.h" #include "Styling/SlateStyle.h" -#include "Widgets/Text/SRichTextBlock.h" +#include "SDialogueTextBlock.h" #include "TimerManager.h" #include @@ -149,6 +149,23 @@ class FPartialDialogueDecorator : public ITextDecorator const int32* CurrentSegmentIndex; }; +void UDialogueTextBlock::SetText(const FText& InText, const FText& InFinalText) +{ + if (SDialogueTextBlock* dialogueTextBlock = static_cast(MyRichTextBlock.Get())) + { + dialogueTextBlock->SetText(dialogueTextBlock->MakeTextAttribute(InText, InFinalText)); + } + else + { + Super::SetText(InText); + } +} + +void UDialogueTextBlock::SetText(const FText& InText) +{ + Super::SetText(InText); +} + TSharedRef UDialogueTextBlock::RebuildWidget() { // Copied from URichTextBlock::RebuildWidget @@ -166,7 +183,7 @@ TSharedRef UDialogueTextBlock::RebuildWidget() } MyRichTextBlock = - SNew(SRichTextBlock) + SNew(SDialogueTextBlock) .TextStyle(bOverrideDefaultStyle ? &DefaultTextStyleOverride : &DefaultTextStyle) .Marshaller(Marshaller); @@ -197,7 +214,7 @@ void UDialogueBox::PlayLine(const FText& InLine) { if (IsValid(LineText)) { - LineText->SetText(FText::GetEmpty()); + LineText->SetText(FText::GetEmpty(), FText::GetEmpty()); } bHasFinishedPlaying = true; @@ -209,7 +226,7 @@ void UDialogueBox::PlayLine(const FText& InLine) { if (IsValid(LineText)) { - LineText->SetText(FText::GetEmpty()); + LineText->SetText(FText::GetEmpty(), CurrentLine); } bHasFinishedPlaying = false; @@ -231,7 +248,7 @@ void UDialogueBox::SkipToLineEnd() CurrentLetterIndex = MaxLetterIndex - 1; if (IsValid(LineText)) { - LineText->SetText(FText::FromString(CalculateSegments())); + LineText->SetText(FText::FromString(CalculateSegments()), CurrentLine); } bHasFinishedPlaying = true; @@ -259,7 +276,7 @@ void UDialogueBox::PlayNextLetter() { if (IsValid(LineText)) { - LineText->SetText(FText::FromString(WrappedString)); + LineText->SetText(FText::FromString(WrappedString), CurrentLine); } OnPlayLetter(); @@ -269,7 +286,7 @@ void UDialogueBox::PlayNextLetter() { if (IsValid(LineText)) { - LineText->SetText(FText::FromString(CalculateSegments())); + LineText->SetText(FText::FromString(CalculateSegments()), CurrentLine); } FTimerManager& TimerManager = GetWorld()->GetTimerManager(); diff --git a/DialogueBox.h b/DialogueBox.h index c69f297..ad71aeb 100644 --- a/DialogueBox.h +++ b/DialogueBox.h @@ -31,7 +31,13 @@ class UDialogueTextBlock : public URichTextBlock CurrentSegmentIndex = InCurrentSegmentIndex; } + // variant method to feed slate widget more info + void SetText(const FText& InText, const FText& InFinalText); + protected: + // implementation hidden in favour of two parameter variant + void SetText(const FText& InText) override; + virtual TSharedRef RebuildWidget() override; private: diff --git a/SDialogueTextBlock.cpp b/SDialogueTextBlock.cpp new file mode 100644 index 0000000..b389ea1 --- /dev/null +++ b/SDialogueTextBlock.cpp @@ -0,0 +1,32 @@ +#include "SDialogueTextBlock.h" + +TAttribute SDialogueTextBlock::MakeTextAttribute(const FText& typedText, const FText& finalText) const +{ + return TAttribute::CreateRaw(this, &SDialogueTextBlock::GetTextInternal, typedText, finalText); +} + +FVector2D SDialogueTextBlock::ComputeDesiredSize(float LayoutScaleMultiplier) const +{ + // calculate actual maxmimum dialogue size + isComputingDesiredSize = true; + auto result = SRichTextBlock::ComputeDesiredSize(LayoutScaleMultiplier); + isComputingDesiredSize = false; + + // poke the method again because this internally caches some junk pertaining to layout/content + (void)SRichTextBlock::ComputeDesiredSize(LayoutScaleMultiplier); + + // return the overridden value + return result; +} + +FText SDialogueTextBlock::GetTextInternal(FText typedText, FText finalText) const +{ + if (isComputingDesiredSize) + { + return finalText; + } + else + { + return typedText; + } +} diff --git a/SDialogueTextBlock.h b/SDialogueTextBlock.h new file mode 100644 index 0000000..622d81d --- /dev/null +++ b/SDialogueTextBlock.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +class SDialogueTextBlock : public SRichTextBlock +{ +public: + TAttribute MakeTextAttribute(const FText& typedText, const FText& finalText) const; + +protected: + FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override; + +private: + FText GetTextInternal(FText typedText, FText finalText) const; + + mutable bool isComputingDesiredSize; +}; From d8a1c772adce7b72f24ad3caaeb6c10c3e37e7d5 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sun, 23 Jul 2023 04:33:58 +0100 Subject: [PATCH 08/12] - Minor amendment to custom slate widget. Feed in trivial text attribute when dialogue demands it. --- DialogueBox.cpp | 16 +++++++++------- DialogueBox.h | 12 ++++++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/DialogueBox.cpp b/DialogueBox.cpp index 50525a0..7a19614 100644 --- a/DialogueBox.cpp +++ b/DialogueBox.cpp @@ -149,10 +149,12 @@ class FPartialDialogueDecorator : public ITextDecorator const int32* CurrentSegmentIndex; }; -void UDialogueTextBlock::SetText(const FText& InText, const FText& InFinalText) +void UDialogueTextBlock::SetTextPartiallyTyped(const FText& InText, const FText& InFinalText) { + ASSERT(!InText.IdenticalTo(InFinalText)); if (SDialogueTextBlock* dialogueTextBlock = static_cast(MyRichTextBlock.Get())) { + Text = FText::GetEmpty(); dialogueTextBlock->SetText(dialogueTextBlock->MakeTextAttribute(InText, InFinalText)); } else @@ -161,7 +163,7 @@ void UDialogueTextBlock::SetText(const FText& InText, const FText& InFinalText) } } -void UDialogueTextBlock::SetText(const FText& InText) +void UDialogueTextBlock::SetTextFullyTyped(const FText& InText) { Super::SetText(InText); } @@ -214,7 +216,7 @@ void UDialogueBox::PlayLine(const FText& InLine) { if (IsValid(LineText)) { - LineText->SetText(FText::GetEmpty(), FText::GetEmpty()); + LineText->SetTextFullyTyped(FText::GetEmpty()); } bHasFinishedPlaying = true; @@ -226,7 +228,7 @@ void UDialogueBox::PlayLine(const FText& InLine) { if (IsValid(LineText)) { - LineText->SetText(FText::GetEmpty(), CurrentLine); + LineText->SetTextPartiallyTyped(FText::GetEmpty(), CurrentLine); } bHasFinishedPlaying = false; @@ -248,7 +250,7 @@ void UDialogueBox::SkipToLineEnd() CurrentLetterIndex = MaxLetterIndex - 1; if (IsValid(LineText)) { - LineText->SetText(FText::FromString(CalculateSegments()), CurrentLine); + LineText->SetTextFullyTyped(CurrentLine); } bHasFinishedPlaying = true; @@ -276,7 +278,7 @@ void UDialogueBox::PlayNextLetter() { if (IsValid(LineText)) { - LineText->SetText(FText::FromString(WrappedString), CurrentLine); + LineText->SetTextPartiallyTyped(FText::FromString(WrappedString), CurrentLine); } OnPlayLetter(); @@ -286,7 +288,7 @@ void UDialogueBox::PlayNextLetter() { if (IsValid(LineText)) { - LineText->SetText(FText::FromString(CalculateSegments()), CurrentLine); + LineText->SetTextFullyTyped(CurrentLine); } FTimerManager& TimerManager = GetWorld()->GetTimerManager(); diff --git a/DialogueBox.h b/DialogueBox.h index ad71aeb..29ff0a2 100644 --- a/DialogueBox.h +++ b/DialogueBox.h @@ -31,12 +31,16 @@ class UDialogueTextBlock : public URichTextBlock CurrentSegmentIndex = InCurrentSegmentIndex; } - // variant method to feed slate widget more info - void SetText(const FText& InText, const FText& InFinalText); + // variants to feed slate widget more info + void SetTextPartiallyTyped(const FText& InText, const FText& InFinalText); + void SetTextFullyTyped(const FText& InText); protected: - // implementation hidden in favour of two parameter variant - void SetText(const FText& InText) override; + // implementation hidden in favour of explicit variants + void SetText(const FText& InText) override + { + URichTextBlock::SetText(InText); + } virtual TSharedRef RebuildWidget() override; From 92202566a2fb38c5311d8693e2db36d0809e617a Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sun, 23 Jul 2023 05:28:24 +0100 Subject: [PATCH 09/12] - Another minor code quality update. --- DialogueBox.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/DialogueBox.cpp b/DialogueBox.cpp index 7a19614..7717f94 100644 --- a/DialogueBox.cpp +++ b/DialogueBox.cpp @@ -151,16 +151,12 @@ class FPartialDialogueDecorator : public ITextDecorator void UDialogueTextBlock::SetTextPartiallyTyped(const FText& InText, const FText& InFinalText) { - ASSERT(!InText.IdenticalTo(InFinalText)); + Super::SetText(InText); + if (SDialogueTextBlock* dialogueTextBlock = static_cast(MyRichTextBlock.Get())) { - Text = FText::GetEmpty(); dialogueTextBlock->SetText(dialogueTextBlock->MakeTextAttribute(InText, InFinalText)); } - else - { - Super::SetText(InText); - } } void UDialogueTextBlock::SetTextFullyTyped(const FText& InText) From 9a5b0e36a7660c13d58a12831df2ef208ba6146b Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Fri, 28 Jul 2023 11:32:29 +0100 Subject: [PATCH 10/12] - (Minor) Cache SDialogueTextBlock desired size. --- SDialogueTextBlock.cpp | 10 +++++++--- SDialogueTextBlock.h | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/SDialogueTextBlock.cpp b/SDialogueTextBlock.cpp index b389ea1..8258010 100644 --- a/SDialogueTextBlock.cpp +++ b/SDialogueTextBlock.cpp @@ -6,17 +6,21 @@ TAttribute SDialogueTextBlock::MakeTextAttribute(const FText& typedText, } FVector2D SDialogueTextBlock::ComputeDesiredSize(float LayoutScaleMultiplier) const +{ + return m_cachedDesiredSize; +} + +void SDialogueTextBlock::CacheDesiredSize(float LayoutScaleMultiplier) { // calculate actual maxmimum dialogue size isComputingDesiredSize = true; - auto result = SRichTextBlock::ComputeDesiredSize(LayoutScaleMultiplier); + m_cachedDesiredSize = SRichTextBlock::ComputeDesiredSize(LayoutScaleMultiplier); isComputingDesiredSize = false; // poke the method again because this internally caches some junk pertaining to layout/content (void)SRichTextBlock::ComputeDesiredSize(LayoutScaleMultiplier); - // return the overridden value - return result; + SRichTextBlock::CacheDesiredSize(LayoutScaleMultiplier); } FText SDialogueTextBlock::GetTextInternal(FText typedText, FText finalText) const diff --git a/SDialogueTextBlock.h b/SDialogueTextBlock.h index 622d81d..7c02af5 100644 --- a/SDialogueTextBlock.h +++ b/SDialogueTextBlock.h @@ -8,10 +8,12 @@ class SDialogueTextBlock : public SRichTextBlock TAttribute MakeTextAttribute(const FText& typedText, const FText& finalText) const; protected: + void CacheDesiredSize(float LayoutScaleMultiplier) override; FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override; private: FText GetTextInternal(FText typedText, FText finalText) const; mutable bool isComputingDesiredSize; + FVector2D m_cachedDesiredSize; }; From adbf4021c8754070c5f9b2dc46990a0d01561f50 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sat, 17 May 2025 13:09:36 +0100 Subject: [PATCH 11/12] - Partially separated some of the logic that relates to parsing wrapped text chunks from the widget itself. Changes currently minified for easy diffing. - Expanded API to allow for partial playback of the provided line. See SetLine() and PlayUntil(). --- DialogueBox.cpp | 101 +++++++++++++++++++++++++++++------------------- DialogueBox.h | 87 ++++++++++++++++++++++++++++++++--------- 2 files changed, 129 insertions(+), 59 deletions(-) diff --git a/DialogueBox.cpp b/DialogueBox.cpp index 7717f94..454e307 100644 --- a/DialogueBox.cpp +++ b/DialogueBox.cpp @@ -194,37 +194,47 @@ UDialogueBox::UDialogueBox(const FObjectInitializer& ObjectInitializer) bHasFinishedPlaying = true; } -void UDialogueBox::PlayLine(const FText& InLine) +void UDialogueBox::SetLine(const FText& InLine) { check(GetWorld()); - FTimerManager& TimerManager = GetWorld()->GetTimerManager(); - TimerManager.ClearTimer(LetterTimer); - CurrentLine = InLine; - CurrentLetterIndex = 0; - CurrentSegmentIndex = 0; + BuiltString = WrappedString(LineText, CurrentLine); + BuiltStringIterator = WrappedStringIterator(*BuiltString); + MaxLetterIndex = 0; - Segments.Empty(); - CachedSegmentText.Empty(); +} + +void UDialogueBox::PlayLine(const FText& InLine) +{ + SetLine(InLine); + PlayToEnd(); +} + +void UDialogueBox::PlayToEnd() +{ + PlayUntil(BuiltString->MaxLetterIndex); +} +void UDialogueBox::PlayUntil(int32 idx) +{ + check(BuiltString); + check(BuiltStringIterator); + + MaxLetterIndex = idx; + + FTimerManager& TimerManager = GetWorld()->GetTimerManager(); + TimerManager.ClearTimer(LetterTimer); if (CurrentLine.IsEmpty()) { - if (IsValid(LineText)) - { - LineText->SetTextFullyTyped(FText::GetEmpty()); - } - bHasFinishedPlaying = true; - OnLineFinishedPlaying(); - - SetVisibility(ESlateVisibility::Hidden); + OnLineFinishedPlaying.Broadcast(); } else { if (IsValid(LineText)) { - LineText->SetTextPartiallyTyped(FText::GetEmpty(), CurrentLine); + LineText->SetTextPartiallyTyped(BuiltStringIterator->get(), CurrentLine); } bHasFinishedPlaying = false; @@ -233,8 +243,6 @@ void UDialogueBox::PlayLine(const FText& InLine) Delegate.BindUObject(this, &ThisClass::PlayNextLetter); TimerManager.SetTimer(LetterTimer, Delegate, LetterPlayTime, true); - - SetVisibility(ESlateVisibility::SelfHitTestInvisible); } } @@ -243,48 +251,61 @@ void UDialogueBox::SkipToLineEnd() FTimerManager& TimerManager = GetWorld()->GetTimerManager(); TimerManager.ClearTimer(LetterTimer); - CurrentLetterIndex = MaxLetterIndex - 1; + BuiltStringIterator->setCurrentLetterIndex(MaxLetterIndex); if (IsValid(LineText)) { - LineText->SetTextFullyTyped(CurrentLine); + if (MaxLetterIndex == BuiltString->MaxLetterIndex) + { + LineText->SetTextFullyTyped(CurrentLine); + } + else + { + LineText->SetTextPartiallyTyped(BuiltStringIterator->get(), CurrentLine); + } } - bHasFinishedPlaying = true; - OnLineFinishedPlaying(); + if (MaxLetterIndex == BuiltString->MaxLetterIndex) + { + bHasFinishedPlaying = true; + } + OnLineFinishedPlaying.Broadcast(); } void UDialogueBox::NativeOnInitialized() { Super::NativeOnInitialized(); - LineText->ConfigureFromParent(&Segments, &CurrentSegmentIndex); + if (BuiltString && BuiltStringIterator) + { + LineText->ConfigureFromParent(&BuiltString->Segments, &BuiltStringIterator->getCurrentSegmentIndex()); + } } void UDialogueBox::PlayNextLetter() { - if (Segments.IsEmpty()) - { - CalculateWrappedString(); - } - - FString WrappedString = CalculateSegments(); - // TODO: How do we keep indexing of text i18n-friendly? - if (CurrentLetterIndex < MaxLetterIndex) + if (BuiltStringIterator->getCurrentLetterIndex() < MaxLetterIndex) { if (IsValid(LineText)) { - LineText->SetTextPartiallyTyped(FText::FromString(WrappedString), CurrentLine); + LineText->SetTextPartiallyTyped(BuiltStringIterator->get(), CurrentLine); } - OnPlayLetter(); - ++CurrentLetterIndex; + OnPlayLetter.Broadcast(); + ++(*BuiltStringIterator); } else { if (IsValid(LineText)) { - LineText->SetTextFullyTyped(CurrentLine); + if (MaxLetterIndex == BuiltString->MaxLetterIndex) + { + LineText->SetTextFullyTyped(CurrentLine); + } + else + { + LineText->SetTextPartiallyTyped(BuiltStringIterator->get(), CurrentLine); + } } FTimerManager& TimerManager = GetWorld()->GetTimerManager(); @@ -297,7 +318,7 @@ void UDialogueBox::PlayNextLetter() } } -void UDialogueBox::CalculateWrappedString() +UDialogueBox::WrappedString::WrappedString(UDialogueTextBlock* LineText, const FText& CurrentLine) { if (IsValid(LineText) && LineText->GetTextParser().IsValid()) { @@ -335,11 +356,11 @@ void UDialogueBox::CalculateWrappedString() } } -FString UDialogueBox::CalculateSegments() +FString UDialogueBox::WrappedStringIterator::evaluate() { - while (CurrentSegmentIndex < Segments.Num()) + while (CurrentSegmentIndex < m_parent.Segments.Num()) { - const FDialogueTextSegment& Segment = Segments[CurrentSegmentIndex]; + const FDialogueTextSegment& Segment = m_parent.Segments[CurrentSegmentIndex]; int32 SegmentStartIndex = std::max(Segment.RunInfo.OriginalRange.BeginIndex, Segment.RunInfo.ContentRange.BeginIndex); CurrentLetterIndex = std::max(CurrentLetterIndex, SegmentStartIndex); diff --git a/DialogueBox.h b/DialogueBox.h index 29ff0a2..e16207d 100644 --- a/DialogueBox.h +++ b/DialogueBox.h @@ -15,7 +15,7 @@ struct FDialogueTextSegment; * A text block that exposes more information about text layout. */ UCLASS() -class UDialogueTextBlock : public URichTextBlock +class SALCORE_GAME_API UDialogueTextBlock : public URichTextBlock { GENERATED_BODY() @@ -51,14 +51,14 @@ class UDialogueTextBlock : public URichTextBlock const int32* CurrentSegmentIndex; }; -struct FDialogueTextSegment +struct SALCORE_GAME_API FDialogueTextSegment { FString Text; FTextRunParseResults RunInfo; }; UCLASS() -class UDialogueBox : public UUserWidget +class SALCORE_GAME_API UDialogueBox : public UUserWidget { GENERATED_BODY() @@ -77,46 +77,95 @@ class UDialogueBox : public UUserWidget UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue Box") float EndHoldTime = 0.15f; + // Initialise future contents of dialogue box, but do not begin playing yet. + UFUNCTION(BlueprintCallable, Category = "Dialogue Box") + void SetLine(const FText& InLine); UFUNCTION(BlueprintCallable, Category = "Dialogue Box") void PlayLine(const FText& InLine); + UFUNCTION(BlueprintCallable, Category = "Dialogue Box") + void PlayToEnd(); + UFUNCTION(BlueprintCallable, Category = "Dialogue Box") + void PlayUntil(int32 idx); + UFUNCTION(BlueprintCallable, Category = "Dialogue Box") void GetCurrentLine(FText& OutLine) const { OutLine = CurrentLine; } UFUNCTION(BlueprintCallable, Category = "Dialogue Box") bool HasFinishedPlayingLine() const { return bHasFinishedPlaying; } + UFUNCTION(BlueprintCallable, Category = "Dialogue Box") + bool HasFinishedPlayingAnimation() const { return !LetterTimer.IsValid(); } UFUNCTION(BlueprintCallable, Category = "Dialogue Box") void SkipToLineEnd(); -protected: - UFUNCTION(BlueprintImplementableEvent, Category = "Dialogue Box") - void OnPlayLetter(); + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDialogueBoxOnPlayLetter); + UPROPERTY(BlueprintAssignable, Category = "Dialogue Box") + FDialogueBoxOnPlayLetter OnPlayLetter; - UFUNCTION(BlueprintImplementableEvent, Category = "Dialogue Box") - void OnLineFinishedPlaying(); + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDialogueBoxOnLineFinishedPlaying); + UPROPERTY(BlueprintAssignable, Category = "Dialogue Box") + FDialogueBoxOnLineFinishedPlaying OnLineFinishedPlaying; +protected: void NativeOnInitialized() override; private: void PlayNextLetter(); - void CalculateWrappedString(); - FString CalculateSegments(); + struct WrappedString + { + WrappedString(UDialogueTextBlock* LineText, const FText& CurrentLine); + + TArray Segments; + int32 MaxLetterIndex; + }; + class WrappedStringIterator + { + public: + WrappedStringIterator(const WrappedString& parent) + : + m_parent(parent) + { + } + + void operator++() + { + CurrentLetterIndex++; + CachedResultText = FText::FromString(evaluate()); + } + const FText& get() const + { + return CachedResultText; + } + + const int32& getCurrentSegmentIndex() const { return CurrentSegmentIndex; } + void setCurrentLetterIndex(int32 idx) { CurrentLetterIndex = idx; CachedResultText = FText::FromString(evaluate()); } + const int32& getCurrentLetterIndex () const { return CurrentLetterIndex; } + + private: + FString evaluate(); + + // The section of the text that's already been printed out and won't ever change. + // This lets us cache some of the work we've already done. We can't cache absolutely + // everything as the last few characters of a string may change if they're related to + // a named run that hasn't been completed yet. + FString CachedSegmentText; + + FText CachedResultText; + + int32 CurrentSegmentIndex = 0; + int32 CurrentLetterIndex = 0; + + const WrappedString& m_parent; + }; UPROPERTY() FText CurrentLine; - TArray Segments; - - // The section of the text that's already been printed out and won't ever change. - // This lets us cache some of the work we've already done. We can't cache absolutely - // everything as the last few characters of a string may change if they're related to - // a named run that hasn't been completed yet. - FString CachedSegmentText; + TOptional BuiltString; + TOptional BuiltStringIterator; - int32 CurrentSegmentIndex = 0; - int32 CurrentLetterIndex = 0; int32 MaxLetterIndex = 0; uint32 bHasFinishedPlaying : 1; From 7be204735ec085636dac59b3d97323b0813b5ca6 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Mon, 29 Dec 2025 07:17:00 +0000 Subject: [PATCH 12/12] - minor api tweak for future ue 5.8 compatibility. --- DialogueBox.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/DialogueBox.cpp b/DialogueBox.cpp index 454e307..6efe7ac 100644 --- a/DialogueBox.cpp +++ b/DialogueBox.cpp @@ -10,6 +10,10 @@ #include #include +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 +#include +#endif + /** * Text run that represents a segment of text which is in the process of being typed out. * The size of the text block will represent the final size of each word rather than the provided content. @@ -60,7 +64,11 @@ class FPartialDialogueRun : public FSlateTextRun for (int32 i = 0; i < futureContent.Len(); ++i) { TCHAR futureChar = futureContent[i]; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + if (FTextChar::IsWhitespace(futureChar)) +#else if (FText::IsWhitespace(futureChar)) +#endif { futureContent.LeftInline(i); break;