diff --git a/Source/StevesUEHelpers/Private/StevesUI/TypewriterTextWidget.cpp b/Source/StevesUEHelpers/Private/StevesUI/TypewriterTextWidget.cpp index 45a28f2..8c70c16 100644 --- a/Source/StevesUEHelpers/Private/StevesUI/TypewriterTextWidget.cpp +++ b/Source/StevesUEHelpers/Private/StevesUI/TypewriterTextWidget.cpp @@ -69,13 +69,19 @@ FText UTypewriterTextWidget::GetText() const } void UTypewriterTextWidget::PlayLine(const FText& InLine, float Speed) +{ + CurrentLine = InLine; + RemainingLinePart = CurrentLine.ToString(); + PlayNextLinePart(Speed); +} + +void UTypewriterTextWidget::PlayNextLinePart(float Speed) { check(GetWorld()); FTimerManager& TimerManager = GetWorld()->GetTimerManager(); TimerManager.ClearTimer(LetterTimer); - CurrentLine = InLine; CurrentRunName = ""; CurrentLetterIndex = 0; CachedLetterIndex = 0; @@ -87,7 +93,7 @@ void UTypewriterTextWidget::PlayLine(const FText& InLine, float Speed) Segments.Empty(); CachedSegmentText.Empty(); - if (CurrentLine.IsEmpty()) + if (RemainingLinePart.IsEmpty()) { if (IsValid(LineText)) { @@ -107,6 +113,7 @@ void UTypewriterTextWidget::PlayLine(const FText& InLine, float Speed) LineText->SetText(FText::GetEmpty()); } + bHasMoreLineParts = false; bHasFinishedPlaying = false; if (bFirstPlayLine) @@ -127,7 +134,20 @@ void UTypewriterTextWidget::PlayLine(const FText& InLine, float Speed) void UTypewriterTextWidget::StartPlayLine() { - CalculateWrappedString(); + CalculateWrappedString(RemainingLinePart); + + if (MaxNumberOfLines > 0 && NumberOfLines > MaxNumberOfLines) + { + int MaxLength = CalculateMaxLength(); + int TerminatorIndex = FindLastTerminator(RemainingLinePart, MaxLength); + int Length = TerminatorIndex + 1; + const FString& FirstLinePart = RemainingLinePart.Left(Length); + + CalculateWrappedString(FirstLinePart); + + RemainingLinePart.RightChopInline(Length); + bHasMoreLineParts = true; + } FTimerDelegate Delegate; Delegate.BindUObject(this, &ThisClass::PlayNextLetter); @@ -214,7 +234,56 @@ bool UTypewriterTextWidget::IsSentenceTerminator(TCHAR Letter) return Letter == '.' || Letter == '!' || Letter == '?'; } -void UTypewriterTextWidget::CalculateWrappedString() +bool UTypewriterTextWidget::IsClauseTerminator(TCHAR Letter) +{ + return Letter == ',' || Letter == ';'; +} + +int UTypewriterTextWidget::FindLastTerminator(const FString& CurrentLineString, int Count) +{ + int TerminatorIndex = CurrentLineString.FindLastCharByPredicate(IsSentenceTerminator, Count); + if (TerminatorIndex != INDEX_NONE) + { + return TerminatorIndex; + } + + TerminatorIndex = CurrentLineString.FindLastCharByPredicate(IsClauseTerminator, Count); + if (TerminatorIndex != INDEX_NONE) + { + return TerminatorIndex; + } + + TerminatorIndex = CurrentLineString.FindLastCharByPredicate(FText::IsWhitespace, Count); + if (TerminatorIndex != INDEX_NONE) + { + return TerminatorIndex; + } + + return (Count - 1); +} + +int UTypewriterTextWidget::CalculateMaxLength() +{ + int MaxLength = 0; + int CurrentNumberOfLines = 1; + for (int i = 0; i < Segments.Num(); i++) + { + const FTypewriterTextSegment& Segment = Segments[i]; + MaxLength += Segment.Text.Len(); + if (Segment.Text.Equals(FString(TEXT("\n")))) + { + CurrentNumberOfLines++; + if (MaxNumberOfLines > 0 && CurrentNumberOfLines > MaxNumberOfLines) + { + break; + } + } + } + + return MaxLength; +} + +void UTypewriterTextWidget::CalculateWrappedString(const FString& CurrentLineString) { // Rich Text views give you: // - A blank block at the start for some reason @@ -223,6 +292,9 @@ void UTypewriterTextWidget::CalculateWrappedString() // - The newlines we add are the only newlines in the output so that's the number of lines // If we've got here, that means the text isn't empty so 1 line at least NumberOfLines = 1; + MaxLetterIndex = 0; + CombinedTextHeight = 0; + Segments.Empty(); if (IsValid(LineText) && LineText->GetTextLayout().IsValid()) { TSharedPtr Layout = LineText->GetTextLayout(); @@ -233,7 +305,7 @@ void UTypewriterTextWidget::CalculateWrappedString() Layout->ClearLines(); Layout->SetWrappingWidth(TextBoxSize.X); - Marshaller->SetText(CurrentLine.ToString(), *Layout.Get()); + Marshaller->SetText(CurrentLineString, *Layout.Get()); Layout->UpdateLayout(); bool bHasWrittenText = false; @@ -299,7 +371,7 @@ void UTypewriterTextWidget::CalculateWrappedString() } else { - Segments.Add(FTypewriterTextSegment{CurrentLine.ToString()}); + Segments.Add(FTypewriterTextSegment{CurrentLineString}); MaxLetterIndex = Segments[0].Text.Len(); } diff --git a/Source/StevesUEHelpers/Public/StevesUI/TypewriterTextWidget.h b/Source/StevesUEHelpers/Public/StevesUI/TypewriterTextWidget.h index e7e5815..ead9679 100644 --- a/Source/StevesUEHelpers/Public/StevesUI/TypewriterTextWidget.h +++ b/Source/StevesUEHelpers/Public/StevesUI/TypewriterTextWidget.h @@ -84,6 +84,10 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter") float PauseTimeAtSentenceTerminators = 0.5f; + /// If set > 0, splits a single PlayLine into multiple segments of this number of lines maximum + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter") + int MaxNumberOfLines = 0; + /// Set Text immediately UFUNCTION(BlueprintCallable) void SetText(const FText& InText); @@ -92,16 +96,33 @@ public: UFUNCTION(BlueprintCallable) FText GetText() const; - + + /** + * Play a line of text. + * Note: if, when line splits are calculated, this line exceeds MaxNumberOfLines, then only this number of lines + * will be played by this call. In that case, HasMoreLineParts() will return true, and you will need to call + * PlayNextLinePart() to play the remainder of the line. + * @param InLine The input line + * @param Speed + */ UFUNCTION(BlueprintCallable, Category = "Typewriter") void PlayLine(const FText& InLine, float Speed = 1.0f); UFUNCTION(BlueprintCallable, Category = "Typewriter") void GetCurrentLine(FText& OutLine) const { OutLine = CurrentLine; } + /// Return whether the entire line has finished playing UFUNCTION(BlueprintCallable, Category = "Typewriter") bool HasFinishedPlayingLine() const { return bHasFinishedPlaying; } + /// Returns whether the number of lines exceeded MaxNumberOfLines and there are still parts to play. + UFUNCTION(BlueprintCallable, Category = "Typewriter") + bool HasMoreLineParts() const { return bHasMoreLineParts; } + + /// If HasMoreLineParts() is true, play the next part of the line originally requested by PlayLine + UFUNCTION(BlueprintCallable, Category = "Typewriter") + void PlayNextLinePart(float Speed = 1.0f); + UFUNCTION(BlueprintCallable, Category = "Typewriter") void SkipToLineEnd(); @@ -127,15 +148,18 @@ protected: private: void PlayNextLetter(); static bool IsSentenceTerminator(TCHAR Letter); + static bool IsClauseTerminator(TCHAR Letter); + static int FindLastTerminator(const FString& CurrentLineString, int Count); - void CalculateWrappedString(); + int CalculateMaxLength(); + void CalculateWrappedString(const FString& CurrentLineString); FString CalculateSegments(FString* OutCurrentRunName); void StartPlayLine(); UPROPERTY() FText CurrentLine; - + FString RemainingLinePart; struct FTypewriterTextSegment { @@ -158,6 +182,7 @@ private: float CombinedTextHeight = 0; uint32 bHasFinishedPlaying : 1; + uint32 bHasMoreLineParts : 1; FTimerHandle LetterTimer; float CurrentPlaySpeed = 1;