mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
351 lines
14 KiB
C++
351 lines
14 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE framework.
|
|
Copyright (c) Raw Material Software Limited
|
|
|
|
JUCE is an open source framework subject to commercial or open source
|
|
licensing.
|
|
|
|
By downloading, installing, or using the JUCE framework, or combining the
|
|
JUCE framework with any other source code, object code, content or any other
|
|
copyrightable work, you agree to the terms of the JUCE End User Licence
|
|
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
|
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
|
do not agree to the terms of these agreements, we will not license the JUCE
|
|
framework to you, and you must discontinue the installation or download
|
|
process and cease use of the JUCE framework.
|
|
|
|
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
|
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
|
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
|
|
|
Or:
|
|
|
|
You may also use this code under the terms of the AGPLv3:
|
|
https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
|
|
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
|
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
|
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
namespace juce::detail
|
|
{
|
|
|
|
/** Types of text direction. This may also be applied to characters. */
|
|
enum class TextDirection
|
|
{
|
|
ltr, // This text reads left to right.
|
|
rtl // This text reads right to left.
|
|
};
|
|
|
|
class ShapedTextOptions
|
|
{
|
|
private:
|
|
auto tie() const
|
|
{
|
|
return std::tie (justification,
|
|
readingDir,
|
|
wordWrapWidth,
|
|
alignmentWidth,
|
|
height,
|
|
fontsForRange,
|
|
firstLineIndent,
|
|
leading,
|
|
additiveLineSpacing,
|
|
baselineAtZero,
|
|
allowBreakingInsideWord,
|
|
trailingWhitespacesShouldFit,
|
|
maxNumLines,
|
|
ellipsis);
|
|
}
|
|
|
|
public:
|
|
bool operator== (const ShapedTextOptions& other) const { return tie() == other.tie(); }
|
|
bool operator!= (const ShapedTextOptions& other) const { return tie() != other.tie(); }
|
|
|
|
//==============================================================================
|
|
[[nodiscard]] ShapedTextOptions withJustification (Justification x) const
|
|
{
|
|
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 withWordWrapWidth (float x) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::wordWrapWidth, 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
|
|
withWordWrapWidth 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 withWordWrapWidth.
|
|
|
|
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 withWordWrapWidth, if alignment is required.
|
|
|
|
With this option off, text is either not aligned, or aligned to the width specified using
|
|
withWordWrapWidth.
|
|
|
|
When this option is in use, it overrides the width specified in withWordWrapWidth 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);
|
|
}
|
|
|
|
[[nodiscard]] ShapedTextOptions withFont (Font x) const
|
|
{
|
|
RangedValues<Font> fonts;
|
|
detail::Ranges::Operations ops;
|
|
fonts.set ({ 0, std::numeric_limits<int64>::max() }, x, ops);
|
|
|
|
return withMember (*this, &ShapedTextOptions::fontsForRange, std::move (fonts));
|
|
}
|
|
|
|
[[nodiscard]] ShapedTextOptions withFonts (const detail::RangedValues<Font>& x) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::fontsForRange, x);
|
|
}
|
|
|
|
[[nodiscard]] ShapedTextOptions withFirstLineIndent (float x) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::firstLineIndent, x);
|
|
}
|
|
|
|
/* This controls the space between lines using a proportional value, with a default of 1.0,
|
|
meaning single line spacing i.e. the descender of the current line + ascender of the next
|
|
line. This value is multiplied by the leading provided here.
|
|
*/
|
|
[[nodiscard]] ShapedTextOptions withLeading (float x) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::leading, x);
|
|
}
|
|
|
|
/* This controls the space between lines using an additive absolute value, with a default of 0.0.
|
|
This value is added to the spacing between each two lines.
|
|
*/
|
|
[[nodiscard]] ShapedTextOptions withAdditiveLineSpacing (float x) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::additiveLineSpacing, x);
|
|
}
|
|
|
|
[[nodiscard]] ShapedTextOptions withBaselineAtZero (bool x = true) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::baselineAtZero, x);
|
|
}
|
|
|
|
[[nodiscard]] ShapedTextOptions withTrailingWhitespacesShouldFit (bool x = true) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::trailingWhitespacesShouldFit, x);
|
|
}
|
|
|
|
[[nodiscard]] ShapedTextOptions withMaxNumLines (int64 x) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::maxNumLines, x);
|
|
}
|
|
|
|
[[nodiscard]] ShapedTextOptions withEllipsis (String x = String::charToString ((juce_wchar) 0x2026)) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::ellipsis, std::move (x));
|
|
}
|
|
|
|
/* Draw each line in its entirety even if it goes beyond wordWrapWidth. This means that even
|
|
if configured, an ellipsis will never be inserted.
|
|
|
|
This is used by the TextEditor where the Viewport guarantees that all text will be viewable
|
|
even beyond the word wrap width.
|
|
*/
|
|
[[nodiscard]] ShapedTextOptions withDrawLinesInFull (bool x = true) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::drawLinesInFull, std::move (x));
|
|
}
|
|
|
|
[[nodiscard]] ShapedTextOptions withReadingDirection (std::optional<TextDirection> x) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::readingDir, x);
|
|
}
|
|
|
|
[[nodiscard]] ShapedTextOptions withAllowBreakingInsideWord (bool x = true) const
|
|
{
|
|
return withMember (*this, &ShapedTextOptions::allowBreakingInsideWord, x);
|
|
}
|
|
|
|
const auto& getReadingDirection() const { return readingDir; }
|
|
const auto& getJustification() const { return justification; }
|
|
const auto& getWordWrapWidth() const { return wordWrapWidth; }
|
|
const auto& getAlignmentWidth() const { return alignmentWidth; }
|
|
const auto& getHeight() const { return height; }
|
|
const auto& getFontsForRange() const { return fontsForRange; }
|
|
const auto& getFirstLineIndent() const { return firstLineIndent; }
|
|
const auto& getLeading() const { return leading; }
|
|
const auto& getAdditiveLineSpacing() const { return additiveLineSpacing; }
|
|
const auto& isBaselineAtZero() const { return baselineAtZero; }
|
|
const auto& getTrailingWhitespacesShouldFit() const { return trailingWhitespacesShouldFit; }
|
|
const auto& getMaxNumLines() const { return maxNumLines; }
|
|
const auto& getEllipsis() const { return ellipsis; }
|
|
const auto& getDrawLinesInFull() const { return drawLinesInFull; }
|
|
const auto& getAllowBreakingInsideWord() const { return allowBreakingInsideWord; }
|
|
|
|
private:
|
|
Justification justification { Justification::topLeft };
|
|
std::optional<TextDirection> readingDir;
|
|
std::optional<float> wordWrapWidth;
|
|
std::optional<float> alignmentWidth;
|
|
std::optional<float> height;
|
|
|
|
detail::RangedValues<Font> fontsForRange = std::invoke ([&]
|
|
{
|
|
detail::RangedValues<Font> result;
|
|
detail::Ranges::Operations ops;
|
|
result.set ({ 0, std::numeric_limits<int64>::max() }, FontOptions { 15.0f }, ops);
|
|
return result;
|
|
});
|
|
|
|
float firstLineIndent = 0.0f;
|
|
float leading = 1.0f;
|
|
float additiveLineSpacing = 0.0f;
|
|
bool baselineAtZero = false;
|
|
bool allowBreakingInsideWord = false;
|
|
bool trailingWhitespacesShouldFit = true;
|
|
bool drawLinesInFull = false;
|
|
int64 maxNumLines = std::numeric_limits<int64>::max();
|
|
String ellipsis;
|
|
};
|
|
|
|
struct ShapedGlyph
|
|
{
|
|
ShapedGlyph (uint32_t glyphIdIn,
|
|
int64 clusterIn,
|
|
bool unsafeToBreakIn,
|
|
bool whitespaceIn,
|
|
bool newlineIn,
|
|
int8_t distanceFromLigatureIn,
|
|
Point<float> advanceIn,
|
|
Point<float> offsetIn)
|
|
: advance (advanceIn),
|
|
offset (offsetIn),
|
|
cluster (clusterIn),
|
|
glyphId (glyphIdIn),
|
|
unsafeToBreak (unsafeToBreakIn),
|
|
whitespace (whitespaceIn),
|
|
newline (newlineIn),
|
|
distanceFromLigature (distanceFromLigatureIn) {}
|
|
|
|
bool isUnsafeToBreak() const { return unsafeToBreak; }
|
|
bool isWhitespace() const { return whitespace; }
|
|
bool isNewline() const { return newline; }
|
|
|
|
bool isNonLigature() const { return distanceFromLigature == 0; }
|
|
bool isLigature() const { return distanceFromLigature < 0; }
|
|
bool isPlaceholderForLigature() const { return distanceFromLigature > 0; }
|
|
|
|
int8_t getDistanceFromLigature() const { return distanceFromLigature; }
|
|
int8_t getNumTrailingLigaturePlaceholders() const { return (int8_t) -distanceFromLigature; }
|
|
|
|
Point<float> advance;
|
|
Point<float> offset;
|
|
int64 cluster;
|
|
uint32_t glyphId;
|
|
|
|
private:
|
|
// These are effectively bools, pack into a single int once we have more than four flags.
|
|
int8_t unsafeToBreak;
|
|
int8_t whitespace;
|
|
int8_t newline;
|
|
int8_t distanceFromLigature;
|
|
};
|
|
|
|
struct GlyphLookupEntry
|
|
{
|
|
Range<int64> glyphRange;
|
|
bool ltr = true;
|
|
};
|
|
|
|
class SimpleShapedText
|
|
{
|
|
public:
|
|
/* Shapes and lays out the first contiguous sequence of ranges specified in the fonts
|
|
parameter.
|
|
*/
|
|
SimpleShapedText (const String* data,
|
|
const ShapedTextOptions& options);
|
|
|
|
const auto& getLineNumbersForGlyphRanges() const { return lineNumbersForGlyphRanges; }
|
|
|
|
const auto& getLineTextRanges() const { return lineTextRanges; }
|
|
|
|
const auto& getResolvedFonts() const { return resolvedFonts; }
|
|
|
|
Range<int64> getTextRange (int64 glyphIndex) const;
|
|
|
|
/* Returns true if the specified glyph is inside to an LTR run.
|
|
*/
|
|
bool isLtr (int64 glyphIndex) const;
|
|
|
|
/* This function may fail to return an out range, even if the provided textRange falls inside
|
|
the string range used for the creation of the ShapedText object.
|
|
|
|
This is because the shaping process could fail due to insufficient glyph resolution to the
|
|
point, where it will produce zero glyphs for the provided string.
|
|
*/
|
|
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const;
|
|
|
|
/* Returns the input codepoint index that follows the glyph in a logical sense. So for LTR text
|
|
this is the cluster number of the glyph to the right. For RTL text it's the one on the left.
|
|
|
|
If there is no subsequent glyph, the returned number is the first Unicode codepoint index
|
|
that isn't covered by the cluster to which the selected glyph belongs, so for the glyph 'o'
|
|
in "hello" this would be 5, given there are no ligatures in use.
|
|
*/
|
|
int64 getTextIndexAfterGlyph (int64 glyphIndex) const;
|
|
|
|
int64 getNumLines() const { return (int64) lineNumbersForGlyphRanges.getRanges().size(); }
|
|
int64 getNumGlyphs() const { return (int64) glyphsInVisualOrder.size(); }
|
|
|
|
juce_wchar getCodepoint (int64 glyphIndex) const;
|
|
|
|
Span<const ShapedGlyph> getGlyphs (Range<int64> glyphRange) const;
|
|
|
|
Span<const ShapedGlyph> getGlyphs() const;
|
|
|
|
const auto& getGlyphLookup() const { return glyphLookup; }
|
|
|
|
private:
|
|
void shape (const String& data,
|
|
const ShapedTextOptions& options);
|
|
|
|
const String& string;
|
|
std::vector<ShapedGlyph> glyphsInVisualOrder;
|
|
detail::RangedValues<int64> lineNumbersForGlyphRanges;
|
|
detail::Ranges lineTextRanges;
|
|
detail::RangedValues<Font> resolvedFonts;
|
|
detail::RangedValues<GlyphLookupEntry> glyphLookup;
|
|
|
|
JUCE_LEAK_DETECTOR (SimpleShapedText)
|
|
};
|
|
|
|
} // namespace juce::detail
|