diff --git a/modules/juce_graphics/detail/juce_JustifiedText.cpp b/modules/juce_graphics/detail/juce_JustifiedText.cpp index 2a4bbb7e28..d996577fe5 100644 --- a/modules/juce_graphics/detail/juce_JustifiedText.cpp +++ b/modules/juce_graphics/detail/juce_JustifiedText.cpp @@ -108,20 +108,31 @@ static float getMainAxisLineLength (Span glyphs, bool trailin struct MainAxisLineAlignment { - float anchor{}, extraWhitespaceAdvance{}, effectiveLineLength; + float anchor{}, extraWhitespaceAdvance{}, effectiveLineLength{}; Range stretchableWhitespaces; }; static MainAxisLineAlignment getMainAxisLineAlignment (Justification justification, Span glyphs, LineLength lineLength, - float maxWidth, + std::optional maxWidthOpt, + std::optional alignmentWidthOpt, bool trailingWhitespacesShouldFit) { const auto effectiveLineLength = (trailingWhitespacesShouldFit ? lineLength.total : lineLength.withoutTrailingWhitespaces); - const auto tooLong = maxWidth + maxWidthTolerance < effectiveLineLength; + const auto alignmentWidth = alignmentWidthOpt.value_or (maxWidthOpt.value_or (0.0f)); + const auto tooLong = alignmentWidth + maxWidthTolerance < effectiveLineLength; + + MainAxisLineAlignment result; + result.effectiveLineLength = effectiveLineLength; + + // The alignment width opt is supporting the TextEditor use-case where all text remains visible + // with scrolling, even if longer than the alignment width. We don't need to worry about the + // front of and RTL text being visually truncated, because nothing is truncated. + if (tooLong && alignmentWidthOpt.has_value()) + return result; const auto mainAxisLineOffset = [&] { @@ -135,19 +146,21 @@ static MainAxisLineAlignment getMainAxisLineAlignment (Justification justificati return glyphs.front().cluster <= glyphs.back().cluster; }(); + // We don't have to align LTR text, but we need to ensure that it's the logical back of + // RTL text that falls outside the bounds. if (approximateIsLeftToRight) return 0.0f; - return maxWidth - effectiveLineLength; + return alignmentWidth - effectiveLineLength; } if (justification.testFlags (Justification::horizontallyCentred)) { - return (maxWidth - lineLength.withoutTrailingWhitespaces) / 2.0f; + return (alignmentWidth - lineLength.withoutTrailingWhitespaces) / 2.0f; } if (justification.testFlags (Justification::right)) - return maxWidth - effectiveLineLength; + return alignmentWidth - effectiveLineLength; return 0.0f; }(); @@ -171,7 +184,7 @@ static MainAxisLineAlignment getMainAxisLineAlignment (Justification justificati - numWhitespaces.leading - numWhitespaces.trailing; - return numWhitespacesBetweenWords > 0 ? (maxWidth - effectiveLineLength) / (float) numWhitespacesBetweenWords + return numWhitespacesBetweenWords > 0 ? (alignmentWidth - effectiveLineLength) / (float) numWhitespacesBetweenWords : 0.0f; }(); @@ -246,13 +259,11 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions auto m = [&] { - if (! options.getMaxWidth().has_value()) - return MainAxisLineAlignment{}; - return getMainAxisLineAlignment (options.getJustification(), glyphs, lineLength, - *options.getMaxWidth(), + options.getMaxWidth(), + options.getAlignmentWidth(), options.getTrailingWhitespacesShouldFit()); }(); @@ -442,7 +453,6 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions if (cutoffAtFront) pushEllipsisGlyphs(); - const auto& range = shapedText.getGlyphs (lastLineVisibleRange); result.insert (result.end(), range.begin(), range.end()); @@ -454,13 +464,11 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions const auto realign = [&] { - if (! options.getMaxWidth().has_value()) - return MainAxisLineAlignment{}; - return getMainAxisLineAlignment (options.getJustification(), lineWithEllipsisGlyphs, getMainAxisLineLength (lineWithEllipsisGlyphs), - *options.getMaxWidth(), + options.getMaxWidth(), + options.getAlignmentWidth(), options.getTrailingWhitespacesShouldFit()); }(); diff --git a/modules/juce_graphics/detail/juce_JustifiedText.h b/modules/juce_graphics/detail/juce_JustifiedText.h index cf0d6307cc..d91d8925db 100644 --- a/modules/juce_graphics/detail/juce_JustifiedText.h +++ b/modules/juce_graphics/detail/juce_JustifiedText.h @@ -128,7 +128,7 @@ public: if (std::exchange (lastLine, lineMetrics.lineNumber) != lineMetrics.lineNumber) anchor = lineMetrics.anchor; - if (range.getStart() != lastGlyph) + if (range.getStart() != lastGlyph && drawType != DrawType::ellipsis) { detail::RangedValues glyphMask; Ranges::Operations ops; diff --git a/modules/juce_graphics/detail/juce_SimpleShapedText.h b/modules/juce_graphics/detail/juce_SimpleShapedText.h index d3cd1635c4..40f418173b 100644 --- a/modules/juce_graphics/detail/juce_SimpleShapedText.h +++ b/modules/juce_graphics/detail/juce_SimpleShapedText.h @@ -50,6 +50,7 @@ private: return std::tie (justification, readingDir, maxWidth, + alignmentWidth, height, fontsForRange, language, @@ -73,11 +74,45 @@ public: return withMember (*this, &ShapedTextOptions::justification, x); } + /* This option will use soft wrapping for lines that are longer than the specified value, + and it will also align each line to this width, using the Justification provided in + withJustification. + + The alignment width can be overriden using withAlignmentWidth, but currently we only need + to do this for the TextEditor. + */ [[nodiscard]] ShapedTextOptions withMaxWidth (float x) const { return withMember (*this, &ShapedTextOptions::maxWidth, x); } + /* With this option each line will be aligned only if it's shorter or equal to the alignment + width. Otherwise, the line's x anchor will be 0.0f. This is in contrast to using + withMaxWidth only, which will modify the x anchor of RTL lines that are too long, to ensure + that it's the logical end of the text that falls outside the visible bounds. + + The alignment width is also a distinct value from the value used for soft wrapping which is + specified using withMaxWidth. + + This option is specifically meant to support an existing TextEditor behaviour, where text + can be aligned even when word wrapping is off. You probably don't need to use this function, + unless you want to reproduce the particular behaviour seen in the TextEditor, and should + only use withMaxWidth, if alignment is required. + + With this option off, text is either not aligned, or aligned to the width specified using + withMaxWidth. + + When this option is in use, it overrides the width specified in withMaxWidth for alignment + purposes, but not for line wrapping purposes. + + It also accommodates the fact that the TextEditor has a scrolling feature and text never + becomes unreachable, even if the lines are longer than the viewport's width. + */ + [[nodiscard]] ShapedTextOptions withAlignmentWidth (float x) const + { + return withMember (*this, &ShapedTextOptions::alignmentWidth, x); + } + [[nodiscard]] ShapedTextOptions withHeight (float x) const { return withMember (*this, &ShapedTextOptions::height, x); @@ -157,6 +192,7 @@ public: const auto& getReadingDirection() const { return readingDir; } const auto& getJustification() const { return justification; } const auto& getMaxWidth() const { return maxWidth; } + const auto& getAlignmentWidth() const { return alignmentWidth; } const auto& getHeight() const { return height; } const auto& getFontsForRange() const { return fontsForRange; } const auto& getLanguage() const { return language; } @@ -173,6 +209,7 @@ private: Justification justification { Justification::topLeft }; std::optional readingDir; std::optional maxWidth; + std::optional alignmentWidth; std::optional height; detail::RangedValues fontsForRange = std::invoke ([&] diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp index c390f9c6d5..adca801da0 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp @@ -917,11 +917,15 @@ std::pair, float> TextEditor::getTextSelectionEdge (int index, Edge void TextEditor::updateBaseShapedTextOptions() { - textStorage->setBaseShapedTextOptions (detail::ShapedText::Options{} - .withMaxWidth ((float) getWordWrapWidth()) - .withTrailingWhitespacesShouldFit (true) - .withJustification (getJustificationType().getOnlyHorizontalFlags()), - passwordCharacter); + auto options = detail::ShapedText::Options{}.withTrailingWhitespacesShouldFit (true) + .withJustification (getJustificationType().getOnlyHorizontalFlags()); + + if (wordWrap) + options = options.withMaxWidth ((float) getMaximumTextWidth()); + else + options = options.withAlignmentWidth ((float) getMaximumTextWidth()); + + textStorage->setBaseShapedTextOptions (options, passwordCharacter); } static auto asInt64Range (Range r)