diff --git a/DialogueBox.cpp b/DialogueBox.cpp index 493fde9..454e307 100644 --- a/DialogueBox.cpp +++ b/DialogueBox.cpp @@ -3,9 +3,167 @@ #include "DialogueBox.h" #include "Engine/Font.h" #include "Styling/SlateStyle.h" -#include "Widgets/Text/SRichTextBlock.h" +#include "SDialogueTextBlock.h" #include "TimerManager.h" +#include +#include +#include + +/** + * 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: + 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) + { + } + + 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); + } + } + +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 + existingChars, Segment.RunInfo.ContentRange.Len() - existingChars); + } + else + { + // no tags + 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]; + if (FText::IsWhitespace(futureChar)) + { + futureContent.LeftInline(i); + break; + } + } + + return *Text + futureContent; + } + + 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; + } + + const FDialogueTextSegment& Segment; +}; + +/** + * A decorator that intercepts partially typed segments and allocates an FPartialDialogueRun to represent them. + */ +class FPartialDialogueDecorator : public ITextDecorator +{ +public: + FPartialDialogueDecorator(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 + { + // copied from FRichTextLayoutMarshaller::AppendRunsForText + 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)); + } + + // 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]; + *InOutModelText += Segment.Text.Mid(Range.BeginIndex - Segment.RunInfo.OriginalRange.BeginIndex, Range.Len()); + + return MakeShared(RunInfo, InOutModelText, Style, ModelRange, Segment); + } + +private: + const TArray* Segments; + const int32* CurrentSegmentIndex; +}; + +void UDialogueTextBlock::SetTextPartiallyTyped(const FText& InText, const FText& InFinalText) +{ + Super::SetText(InText); + + if (SDialogueTextBlock* dialogueTextBlock = static_cast(MyRichTextBlock.Get())) + { + dialogueTextBlock->SetText(dialogueTextBlock->MakeTextAttribute(InText, InFinalText)); + } +} + +void UDialogueTextBlock::SetTextFullyTyped(const FText& InText) +{ + Super::SetText(InText); +} + TSharedRef UDialogueTextBlock::RebuildWidget() { // Copied from URichTextBlock::RebuildWidget @@ -14,18 +172,18 @@ 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) + { + // add custom decorator to intercept partially typed segments + 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(); - })); + SNew(SDialogueTextBlock) + .TextStyle(bOverrideDefaultStyle ? &GetDefaultTextStyleOverride() : &DefaultTextStyle) + .Marshaller(Marshaller); return MyRichTextBlock.ToSharedRef(); } @@ -36,38 +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; - CachedLetterIndex = 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->SetText(FText::GetEmpty()); - } - bHasFinishedPlaying = true; - OnLineFinishedPlaying(); - - SetVisibility(ESlateVisibility::Hidden); + OnLineFinishedPlaying.Broadcast(); } else { if (IsValid(LineText)) { - LineText->SetText(FText::GetEmpty()); + LineText->SetTextPartiallyTyped(BuiltStringIterator->get(), CurrentLine); } bHasFinishedPlaying = false; @@ -76,8 +243,6 @@ void UDialogueBox::PlayLine(const FText& InLine) Delegate.BindUObject(this, &ThisClass::PlayNextLetter); TimerManager.SetTimer(LetterTimer, Delegate, LetterPlayTime, true); - - SetVisibility(ESlateVisibility::SelfHitTestInvisible); } } @@ -86,41 +251,61 @@ void UDialogueBox::SkipToLineEnd() FTimerManager& TimerManager = GetWorld()->GetTimerManager(); TimerManager.ClearTimer(LetterTimer); - CurrentLetterIndex = MaxLetterIndex - 1; + BuiltStringIterator->setCurrentLetterIndex(MaxLetterIndex); if (IsValid(LineText)) { - LineText->SetText(FText::FromString(CalculateSegments())); + 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::PlayNextLetter() +void UDialogueBox::NativeOnInitialized() { - if (Segments.IsEmpty()) + Super::NativeOnInitialized(); + + if (BuiltString && BuiltStringIterator) { - CalculateWrappedString(); + LineText->ConfigureFromParent(&BuiltString->Segments, &BuiltStringIterator->getCurrentSegmentIndex()); } +} - FString WrappedString = CalculateSegments(); - +void UDialogueBox::PlayNextLetter() +{ // TODO: How do we keep indexing of text i18n-friendly? - if (CurrentLetterIndex < MaxLetterIndex) + if (BuiltStringIterator->getCurrentLetterIndex() < MaxLetterIndex) { if (IsValid(LineText)) { - LineText->SetText(FText::FromString(WrappedString)); + LineText->SetTextPartiallyTyped(BuiltStringIterator->get(), CurrentLine); } - OnPlayLetter(); - ++CurrentLetterIndex; + OnPlayLetter.Broadcast(); + ++(*BuiltStringIterator); } else { if (IsValid(LineText)) { - LineText->SetText(FText::FromString(CalculateSegments())); + if (MaxLetterIndex == BuiltString->MaxLetterIndex) + { + LineText->SetTextFullyTyped(CurrentLine); + } + else + { + LineText->SetTextPartiallyTyped(BuiltStringIterator->get(), CurrentLine); + } } FTimerManager& TimerManager = GetWorld()->GetTimerManager(); @@ -133,129 +318,83 @@ void UDialogueBox::PlayNextLetter() } } -// TODO: Need to recalculate this + CalculateSegments when the text box gets resized. -void UDialogueBox::CalculateWrappedString() +UDialogueBox::WrappedString::WrappedString(UDialogueTextBlock* LineText, const FText& CurrentLine) { - 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 UDialogueBox::WrappedStringIterator::evaluate() { - FString Result = CachedSegmentText; - - int32 Idx = CachedLetterIndex; - while (Idx <= CurrentLetterIndex && CurrentSegmentIndex < Segments.Num()) + while (CurrentSegmentIndex < m_parent.Segments.Num()) { - const FDialogueTextSegment& Segment = Segments[CurrentSegmentIndex]; - if (!Segment.RunInfo.Name.IsEmpty()) - { - Result += FString::Printf(TEXT("<%s"), *Segment.RunInfo.Name); + const FDialogueTextSegment& Segment = m_parent.Segments[CurrentSegmentIndex]; - 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? eg. + if (!Segment.RunInfo.Name.IsEmpty() && !Segment.RunInfo.OriginalRange.IsEmpty() && Segment.RunInfo.ContentRange.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 7597a1a..e16207d 100644 --- a/DialogueBox.h +++ b/DialogueBox.h @@ -9,41 +9,56 @@ #include "Framework/Text/SlateTextLayout.h" #include "DialogueBox.generated.h" +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() 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; } + // 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 explicit variants + void SetText(const FText& InText) override + { + URichTextBlock::SetText(InText); + } + virtual TSharedRef RebuildWidget() override; private: - TSharedPtr TextLayout; - TSharedPtr TextMarshaller; + TSharedPtr TextParser; + + const TArray* Segments; + const int32* CurrentSegmentIndex; }; -struct FDialogueTextSegment +struct SALCORE_GAME_API FDialogueTextSegment { FString Text; - FRunInfo RunInfo; + FTextRunParseResults RunInfo; }; UCLASS() -class FLAME_API UDialogueBox : public UUserWidget +class SALCORE_GAME_API UDialogueBox : public UUserWidget { GENERATED_BODY() @@ -62,45 +77,95 @@ class FLAME_API 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; - int32 CachedLetterIndex = 0; + TOptional BuiltString; + TOptional BuiltStringIterator; - int32 CurrentSegmentIndex = 0; - int32 CurrentLetterIndex = 0; int32 MaxLetterIndex = 0; uint32 bHasFinishedPlaying : 1; 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. diff --git a/SDialogueTextBlock.cpp b/SDialogueTextBlock.cpp new file mode 100644 index 0000000..8258010 --- /dev/null +++ b/SDialogueTextBlock.cpp @@ -0,0 +1,36 @@ +#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 +{ + return m_cachedDesiredSize; +} + +void SDialogueTextBlock::CacheDesiredSize(float LayoutScaleMultiplier) +{ + // calculate actual maxmimum dialogue size + isComputingDesiredSize = true; + 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); + + SRichTextBlock::CacheDesiredSize(LayoutScaleMultiplier); +} + +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..7c02af5 --- /dev/null +++ b/SDialogueTextBlock.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +class SDialogueTextBlock : public SRichTextBlock +{ +public: + 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; +};