1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

TextEditor: Fix centred and right alignment when word wrap is disabled

This commit is contained in:
attila 2025-03-19 18:12:55 +01:00 committed by Attila Szarvas
parent 9730cd2808
commit 5f5a247f82
4 changed files with 71 additions and 22 deletions

View file

@ -108,20 +108,31 @@ static float getMainAxisLineLength (Span<const ShapedGlyph> glyphs, bool trailin
struct MainAxisLineAlignment
{
float anchor{}, extraWhitespaceAdvance{}, effectiveLineLength;
float anchor{}, extraWhitespaceAdvance{}, effectiveLineLength{};
Range<int64> stretchableWhitespaces;
};
static MainAxisLineAlignment getMainAxisLineAlignment (Justification justification,
Span<const ShapedGlyph> glyphs,
LineLength lineLength,
float maxWidth,
std::optional<float> maxWidthOpt,
std::optional<float> 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());
}();

View file

@ -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<int8_t> glyphMask;
Ranges::Operations ops;

View file

@ -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<TextDirection> readingDir;
std::optional<float> maxWidth;
std::optional<float> alignmentWidth;
std::optional<float> height;
detail::RangedValues<Font> fontsForRange = std::invoke ([&]

View file

@ -917,11 +917,15 @@ std::pair<Point<float>, 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<int> r)